From 4deac86446ced121c6271b239da3d10b2be9e4d2 Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Sat, 6 Jul 2024 20:30:51 -0700 Subject: [PATCH] Remove sqlite, refactor to use level (#69) * init * checkpoint, db dump method, refactor with-db * fix zod level db fns, migrate "create.ts" from dev_package_examples * wip refactoring dev server to level from sqlite * Added a new test for the `dev_package_examples/list.test.ts` file. * Replaced Kysely query with ZodLevelDatabase dump and mapping to required format. * list endpoint support * wip progress on refactor to level * db reset * implement export file refactor * implement download * fix tests * remove log line * small fixes to prevent level open/close issues * remove sqlite * remove sqlite types --- bun.lockb | Bin 225449 -> 215494 bytes dev-server-api/bun.lockb | Bin 72296 -> 57170 bytes dev-server-api/package.json | 4 - dev-server-api/routes/api/db/download.ts | 25 ++++ .../routes/api/dev_package_examples/create.ts | 40 +++---- .../routes/api/dev_package_examples/get.ts | 33 ++--- .../routes/api/dev_package_examples/list.ts | 39 +++--- .../routes/api/dev_package_examples/update.ts | 76 ++++++------ dev-server-api/routes/api/dev_server/reset.ts | 14 +-- .../routes/api/export_files/create.ts | 23 ++-- .../routes/api/export_files/download.ts | 17 +-- .../routes/api/export_requests/create.ts | 26 ++-- .../routes/api/export_requests/get.ts | 39 +++--- .../routes/api/export_requests/list.ts | 19 ++- .../routes/api/export_requests/update.ts | 36 +++--- .../routes/api/package_info/create.ts | 20 ++-- dev-server-api/routes/api/package_info/get.ts | 13 +- dev-server-api/routes/index.ts | 16 +++ dev-server-api/server.ts | 2 +- dev-server-api/src/db/get-db.ts | 113 ++---------------- dev-server-api/src/db/schema.ts | 17 +-- .../src/db/{level-db.ts => zod-level-db.ts} | 66 ++++++++-- dev-server-api/src/lib/zod/export_file.ts | 8 -- .../src/lib/zod/export_package_info.ts | 5 - dev-server-api/src/lib/zod/export_request.ts | 21 ---- dev-server-api/src/middlewares/with-db.ts | 10 +- dev-server-api/static-routes.ts | 6 +- .../tests/fixtures/get-test-server.ts | 3 +- .../dev_package_examples/create.test.ts | 19 +++ .../routes/dev_package_examples/get.test.ts | 25 ++++ .../routes/dev_package_examples/list.test.ts | 32 +++++ .../dev_package_examples/update.test.ts | 28 +++++ .../tests/routes/export_files/create.test.ts | 18 +++ .../routes/export_files/download.test.ts | 29 +++++ .../routes/export_requests/create.test.ts | 24 ++++ .../tests/routes/export_requests/get.test.ts | 41 +++++++ .../tests/routes/export_requests/list.test.ts | 35 ++++++ .../routes/export_requests/update.test.ts | 50 ++++++++ lib/cmd-fns/dev/index.ts | 25 ++-- package.json | 4 +- 40 files changed, 634 insertions(+), 387 deletions(-) create mode 100644 dev-server-api/routes/api/db/download.ts create mode 100644 dev-server-api/routes/index.ts rename dev-server-api/src/db/{level-db.ts => zod-level-db.ts} (62%) delete mode 100644 dev-server-api/src/lib/zod/export_file.ts delete mode 100644 dev-server-api/src/lib/zod/export_package_info.ts delete mode 100644 dev-server-api/src/lib/zod/export_request.ts create mode 100644 dev-server-api/tests/routes/dev_package_examples/create.test.ts create mode 100644 dev-server-api/tests/routes/dev_package_examples/get.test.ts create mode 100644 dev-server-api/tests/routes/dev_package_examples/list.test.ts create mode 100644 dev-server-api/tests/routes/dev_package_examples/update.test.ts create mode 100644 dev-server-api/tests/routes/export_files/create.test.ts create mode 100644 dev-server-api/tests/routes/export_files/download.test.ts create mode 100644 dev-server-api/tests/routes/export_requests/create.test.ts create mode 100644 dev-server-api/tests/routes/export_requests/get.test.ts create mode 100644 dev-server-api/tests/routes/export_requests/list.test.ts create mode 100644 dev-server-api/tests/routes/export_requests/update.test.ts diff --git a/bun.lockb b/bun.lockb index 5d1528a3e015cd786a8c98311ce8ee4d75d507e7..36a419f54e808904a0d400b2bcf953cf0262ab5b 100755 GIT binary patch delta 36779 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 delta 41837 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%0Aw=1mn>OiFezm3|zERU=D0kQs=?E>4=;o z5>ZK#j6bY|0w!OA4O^m{VFe4ywp!+a^o8b>15VwgGPp7z+0=AfGzMJ(D6AhKf-#yf zSrdDEz!U}%p+2rp!58@D94p|dGz?|qUMP{ch~r(*voRi$44P8l#0KKzJ?iz^BAD&E zO$NG+^;y=9oo$fGY%8g{Q|7uBh{3lwPF!W3xUUVomlah>=IV_P;K=W+3;Q-fTjyf2 zN!MI`D%-Zk!pdxh4A8e!6nc1}3)BP}@!8gEU*rViI4B{75) zVW2dG$Eb_h=MhL1ix(}}@!wc}!y=bX`o)i&e#sw1r7gDZ4`tP|W$-`>x_g(|X=J+$ z%D1YOQOvjOR5ETsu%3e+TvdYi@KG0xPd$WpSc>*;bl~A`fV24DuPlp`NuWoP(vS^j z+h7;NKhCpkcsvEEMEGYKMqUd)D#BPI{Hz-aiEye1jwZueFMP}J$3AEe_m diff --git a/dev-server-api/bun.lockb b/dev-server-api/bun.lockb index 63511a13e143a02a087deaea321122dff5e1644d..ff693776b44a297d6761645da0792a4ed8856b2c 100755 GIT binary patch delta 8130 zcmeHMdsI}{)xUROkc$j&Va5SPQIv;+1Hz!d3^6JiL5*L0re76sz)>CoVx%!L$rqbM zqn2#n##ps#&?HJQNt=hMCT1zV@vTdP$!8lanrKsXY0~t!&%HO;uUTo=YXA8D$XWbm zpZz%doU_k4`(Ez(VwZ30Zl7P-627$Eh};)kKWeriBeU1Z1>dC4zhk>F#PN*-!0k zE@&9!J;*Lc)i-ofT|EsN0;$tql7d0YXHN4}&yu8c@a#PTKP>lv@|2P_c`Ul%UP|!8 zc16{N#cs^)q9%{Sc-g)dl=bEA>QZ;ABz1uvqI$2VreA4UWsx)$fnY~Q{O~|Z3TtLp zAtu%C+8R$uopc5IaLBh{ZXB~)pgf?zz$@EVRu^&zrT-f{J*K;1905B$hIcTkV5wg6 zwxhy9JqyYP#nTJRX2Oxqw?K|R?{55X$Gbpzpidx(EI$Cs?JFzXvpwZ*soYcUDehNN zF&T@*^5LKy@AAT#?tXJhYG#(XYpdaZ5adDiQ1JAN>Le+;i@I<%Wra105CXF|%jvof)$fSVs1Y$I~qVcz}by)wupgfRcpltUpXniCq zTQtE^Q2cr8@Wb*FP;NLDlpExNvV&xej|62qA5gZt0S06Co(E<7k3m_#Q{!I&<@Pqr zoFloSJL1o4*M`|*uwHL>wLuvuJ17R_2(-ukeW)jE@>zw|HST`pHDkc@l9X3_;7O8( zfaeZ+qa$uVyKYutg{QJ2RO5X(^pG!!bEzRO!d@>%!@B^l+3cD!PZ8?-0Up>cP##cB zZ&hDcSY8H^6iQF~M%6zauX@=7%5L8VWiQ)6c^Jiov)$F6iqc$7U(#3IvPHEeB{-Op z$L)4I(g#SlAm`yu1Z6*?HGUM@@rh1KRO8nZJu`m_Jo<_Ec1=<{`WOPv3mCG9$7RUb zVee$n<^_J_n_265<3!%jVYeDmCNGYED`%imnf=MNN7uaP-nGbw4?iFFr=FV*uBux{V$>1v=$eu5HAF1eaKCYRx&OxsLOxf@-DXcQ#` zx#V}r6Xa4PA4$qa*G8%eawys0#)I=Ad$2=(i4uZc$}z|ufy_WnCWqXW8iQT(RJsby zDrB@&X#7Rn6W|7c>m=HmsWHT*6eG6{7H#2O-a!eWF6B>!b04Y- z&!YD5Y$XmcX1#%3AUvK3m;4?zM!1y305uA})P#9gQbHG((g>N>KEk2=QM2eIy0>FF zhrp8>wfW#WX8v39M7orA$ehCBX0Ss^#P;Gj`_j#DhcW{k$6Sr*7V<>740i)*Ta;5y zqN`CZxsVdfF8NLJm|cpD9i~po9^^3QfpgL8!CA^LQM18`2)^t`9*fH`IGAPxIgRs? zLUXCyl4Wc~Z48y0vy`4Vzc@tR0BSu@udPvV zrQld+5SH(N}P^gXTh!4f#Y>DiXol>$LkBOi$m!gsn3w(Q$n6tmvK7I=TIt-%`(1@TArwVjoJuN z%dzO%XHZjR2T@b)!n*3(38<;sU!zta+Ft)&mVqb5XdzpGnyPI@P4!}l*4q}LrjBLn z_p+~0Q(NT3=q;W_ZKUvb6g9P9b2r`2jhZTZ12y4K_9aieOWA@{JP6UiX)!yLHgJ6A zeL0G9A|>>3DYIhrv)t6pp}Yx>?bYSJ2acCpte)cRp{^xb@Q5P6`hLn#5* zU+B>rq+C9Mp!R2Q}gC2FLr;PvngI;P~v|bRzop1U05I-Ar~UGr?(r zK#JJGI*}r-qQ)Un{pBR8IaZt`!wZRYEz4NJ#sWIL7DlGGCg@5uPq z$YXaIY{|3@h84+lEql;Fe@T)UT$tj;Lc*4jIspiRC=XIRLL>}Ek|`%Ait=;(A3#Vw zYY}Hj2J9NesTGAO>(c?&BL<@U5H&!9xQfEm7r^ckI2VvC8wl>OoHCgcS|iUX<1M|1z&0Qpsv2dM(dB!Kmk zg;u2;k^k2CAEMmh6ixq6DYA&@U@9m#ECRSNmFc=Og}!n6*ZYCeU+ zzyB6h7wJFXNDSp)@x!ry0pQp#)HVq!YI%^d!6HrmAZ7WBn*7Ho+b!1Y9;Cb$OEvjY zYRHX~edu~_ck+EGq@H)`%bM=TC?Cj80Q=gk(XF6devIbGy-7yQ-1ij@&O|FckO@55C4zzgHhy%=7A%= zwo}2vku>*76D^)MQYJb#Zvyq5XQIsuN6O3T=E4bd7Tm=rN6ITH0lMd(GSTacDorU^ z?EozJbkN{t`;t2{wFUYr`qwXN?zbe*#~6Tr1v-;`$tG4(>`VT}$MIU<8TLnoy-EL~ z@n^jKhJG(@Zd?A9Ot~w3oBh{LiV3*8lJ}M*eX)v${644tuivSKMK1w77rda0qVt>{ z$7wPGWgC7*ch_XgK-q@VY8=4&6#&aPmFv&wRj9L$AGV1AeAG*;H6iD6yqO8dYcv@r z*JOapT7Vt$gR3vVovj1dA!5h>EsA9PiYDs}%5h~M>ou8~J40E;&QJofX~mvyv~y2v zZ_f7oD0m&<`78%`4&0o(4#uv%{Q8<0PH^SrrHv^anl+l&Fqw9-Nn+CXn5}*{A4)EL5 zqrh0;zktU8Ua9fG1YjbN0XP7D*GvNj0K9UsKo6iN&-nE+ zDHnJM7z|7S_?7K(fM4MT0)v1ofL~^L)%pOB03(6nz{9{0U@DLe^aDl#`2asRh5|)E z4$vPM2Jj1W9=D1^<>x>mkOag7eSt!)o&d^QfR~)>tY^BPLyfZR66z>GOa(j zR(4a?p;GH+v<$|Wc6hd*IuyP;QjDp;4XeA0J~(8N_tBX{X@=u61s*nA_3zp3(F0X7U7-`U~UV%0AjimH0$rY1SToY7mZ$ z|0eE(9{1KmZ%5!Um1Jr-Vv*<4c6iY*AC^61e(&3seprP02udotX`oMGl}uG9EOI)9 zA5F9B7Ze}H-hSWItvxTnwQ6V`i&T>BzBs!(SANK5mZIuPL7El<-QyrZ=~WVcKmtt+z}x=6I@AzebRr+5PJN zVb!0&U;vf_k=;$}j$1PHYlrQNHvIA#)17IeBcazXCaShvl;-rYOz+UN2T}WRvsJy? zkh~QiP0qj57+(higi~GV?ybBfHndu-Kd>J%)cvr8%AwaUOdi{@{&;do+ZXV{dsQs` zhSoIe4-CU@6DQ{h`m!}mK0t9N(yaQW#hf08pL+31cRrjL347s1O_RfE^NF-{{p#aq z`;xz~Jk@8E7_qR@?+h+o`f%O$)DH&w%2?}xHhU^Ipy{L~L%(Af>pYee9TU2>L$BXL znD5=Qk8R0tcW7=z($7wst@^b^|G{gfyyF*a=rD++r6(=v`ZdR1=|*1sRYPuvUcU(O z{4V>|0eQ_IM)gCuv@tWs5 z3}Waq29QGcF#!GI$UoGX^jmq^p$@C@lycf451|pK)8sMq)M<-Vzj6ucbL`d6>MIu* zWQV$0qyy9h+hRHhEB)f;%Xu;Xb2n^xH-pS+7KcOf(s!pV8MyHg7d}-DA63UEmttq8 zL66OVY^&zs6-VctSuEd^4YHPJr1n@EbtYe4LTk?ySVQrwpkLg@M&V71 zL>@yvXA9(9di1OXALM49wOI9QnRhPTx7B{WD^F}-(HmPu(pzU^U6Has$aUS{w)8q(LnDsw@K~1t%>%xY6>9 znSMx~8}!$ZH&xf~UXrN!^3dSgid*CLYMQe?t4&;a$t-tTx!Noz258EP s%4sW`LgZ*wv)3ehxi)3xr-5>>m6;~F4=!Gsu4(EeCOK^7^A@@7Z&WA-OaK4? delta 17277 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 nSzguN<(>Zl?dHl% diff --git a/dev-server-api/package.json b/dev-server-api/package.json index 488a40dc..7e5d7611 100644 --- a/dev-server-api/package.json +++ b/dev-server-api/package.json @@ -7,7 +7,6 @@ "build": "winterspec bundle-routes -i ./routes -o static-routes.ts" }, "devDependencies": { - "@types/better-sqlite3": "^7.6.9", "@types/bun": "latest", "@types/sql.js": "^1.4.9", "make-vfs": "^1.0.10" @@ -16,9 +15,6 @@ "typescript": "^5.0.0" }, "dependencies": { - "better-sqlite3": "^11.0.0", - "kysely": "^0.27.3", - "kysely-bun-sqlite": "^0.3.2", "level": "^8.0.1", "redaxios": "^0.5.1", "winterspec": "0.0.81", diff --git a/dev-server-api/routes/api/db/download.ts b/dev-server-api/routes/api/db/download.ts new file mode 100644 index 00000000..dd1d6f9c --- /dev/null +++ b/dev-server-api/routes/api/db/download.ts @@ -0,0 +1,25 @@ +import { withWinterSpec } from "src/with-winter-spec" +import { z } from "zod" + +export default withWinterSpec({ + methods: ["GET"], + jsonResponse: z.object({ + dev_server_database_dump: z.any(), + }), + auth: "none", +})(async (req, ctx) => { + return new Response( + JSON.stringify( + { + dev_server_database_dump: await ctx.db.dump(), + }, + null, + " " + ), + { + headers: { + "content-type": "application/json", + }, + } + ) +}) diff --git a/dev-server-api/routes/api/dev_package_examples/create.ts b/dev-server-api/routes/api/dev_package_examples/create.ts index 05dae71a..fe95c4e0 100644 --- a/dev-server-api/routes/api/dev_package_examples/create.ts +++ b/dev-server-api/routes/api/dev_package_examples/create.ts @@ -1,5 +1,4 @@ import { withWinterSpec } from "src/with-winter-spec" -import { NotFoundError } from "edgespec/middleware" import { z } from "zod" export default withWinterSpec({ @@ -17,35 +16,26 @@ export default withWinterSpec({ file_path: z.string(), tscircuit_soup: z.any(), error: z.string().nullable().optional(), - last_updated_at: z.string().datetime(), + last_updated_at: z.string().datetime().nullable(), // TODO remove nullable }), }), auth: "none", })(async (req, ctx) => { const tscircuit_soup = req.jsonBody.tscircuit_soup - ? JSON.stringify(req.jsonBody.tscircuit_soup) - : undefined - const dev_package_example = await ctx.db - .insertInto("dev_package_example") - .values({ - file_path: req.jsonBody.file_path, - export_name: req.jsonBody.export_name, - error: req.jsonBody.error, - tscircuit_soup, - is_loading: req.jsonBody.is_loading ? 1 : 0, - last_updated_at: new Date().toISOString(), - }) - .onConflict((oc) => - oc.columns(["file_path"]).doUpdateSet({ - export_name: req.jsonBody.export_name, - error: req.jsonBody.error, - tscircuit_soup, - is_loading: req.jsonBody.is_loading ? 1 : 0, - last_updated_at: new Date().toISOString(), - }) - ) - .returningAll() - .executeTakeFirstOrThrow() + + const existingDevPackageExample = await ctx.db.find("dev_package_example", { + file_path: req.jsonBody.file_path, + }) + + const dev_package_example = await ctx.db.put("dev_package_example", { + dev_package_example_id: existingDevPackageExample?.dev_package_example_id, + file_path: req.jsonBody.file_path, + export_name: req.jsonBody.export_name, + error: req.jsonBody.error, + tscircuit_soup, + is_loading: Boolean(req.jsonBody.is_loading), + last_updated_at: new Date().toISOString(), + }) return ctx.json({ dev_package_example, diff --git a/dev-server-api/routes/api/dev_package_examples/get.ts b/dev-server-api/routes/api/dev_package_examples/get.ts index 4310e0f1..fe909d2f 100644 --- a/dev-server-api/routes/api/dev_package_examples/get.ts +++ b/dev-server-api/routes/api/dev_package_examples/get.ts @@ -12,7 +12,7 @@ export default withWinterSpec({ dev_package_example_id: z.coerce.number(), file_path: z.string(), tscircuit_soup: z.any(), - completed_edit_events: z.array(z.any()).nullable().default(null), + completed_edit_events: z.array(z.any()).default([]), is_loading: z.boolean(), error: z.string().nullable().optional().default(null), soup_last_updated_at: z.string().datetime().nullable().default(null), @@ -26,30 +26,21 @@ export default withWinterSpec({ .datetime() .nullable() .default(null), - last_updated_at: z.string().datetime(), + last_updated_at: z.string().datetime().nullable(), }), }), auth: "none", })(async (req, ctx) => { + const dev_package_example = await ctx.db.get( + "dev_package_example", + req.commonParams.dev_package_example_id + ) + + if (!dev_package_example) { + throw new NotFoundError("Package not found") + } + return ctx.json({ - dev_package_example: await ctx.db - .selectFrom("dev_package_example") - .selectAll() - .where( - "dev_package_example_id", - "=", - req.commonParams.dev_package_example_id - ) - .executeTakeFirstOrThrow((e) => { - throw new NotFoundError("Package not found") - }) - .then((r) => ({ - ...r, - is_loading: r.is_loading === 1, - tscircuit_soup: JSON.parse(r.tscircuit_soup), - completed_edit_events: r.completed_edit_events - ? JSON.parse(r.completed_edit_events) - : null, - })), + dev_package_example, }) }) diff --git a/dev-server-api/routes/api/dev_package_examples/list.ts b/dev-server-api/routes/api/dev_package_examples/list.ts index 842c1f0d..70f058d1 100644 --- a/dev-server-api/routes/api/dev_package_examples/list.ts +++ b/dev-server-api/routes/api/dev_package_examples/list.ts @@ -1,4 +1,3 @@ -import { sql } from "kysely" import { withWinterSpec } from "src/with-winter-spec" import { z } from "zod" @@ -9,32 +8,28 @@ export default withWinterSpec({ z.object({ dev_package_example_id: z.coerce.number(), file_path: z.string(), - export_name: z.string(), - is_loading: z.coerce.boolean(), - edit_events_last_updated_at: z - .string() - .datetime() - .nullable() - .default(null), - soup_last_updated_at: z.string().datetime().nullable().default(null), - last_updated_at: z.string().datetime(), + export_name: z.string().nullable(), + is_loading: z.boolean(), + edit_events_last_updated_at: z.string().datetime().nullable(), + soup_last_updated_at: z.string().datetime().nullable(), + last_updated_at: z.string().datetime().nullable(), }) ), }), auth: "none", })(async (req, ctx) => { - const dev_package_examples = await ctx.db - .selectFrom("dev_package_example") - .select([ - "dev_package_example_id", - "file_path", - "export_name", - "last_updated_at", - "edit_events_last_updated_at", - "soup_last_updated_at", - sql`(is_loading = 1)`.$castTo().as("is_loading"), - ]) - .execute() + const dev_package_examples = (await ctx.db.list("dev_package_example")).map( + (dpe) => ({ + dev_package_example_id: dpe.dev_package_example_id, + file_path: dpe.file_path, + export_name: dpe.export_name, + is_loading: dpe.is_loading, + edit_events_last_updated_at: dpe.edit_events_last_updated_at, + soup_last_updated_at: dpe.soup_last_updated_at, + last_updated_at: dpe.last_updated_at, + }) + ) + return ctx.json({ dev_package_examples, }) diff --git a/dev-server-api/routes/api/dev_package_examples/update.ts b/dev-server-api/routes/api/dev_package_examples/update.ts index 6f6a2d15..6c3c9486 100644 --- a/dev-server-api/routes/api/dev_package_examples/update.ts +++ b/dev-server-api/routes/api/dev_package_examples/update.ts @@ -1,6 +1,7 @@ import { withWinterSpec } from "src/with-winter-spec" import { NotFoundError } from "edgespec/middleware" import { z } from "zod" +import { DevPackageExampleSchema } from "src/db/schema" export default withWinterSpec({ methods: ["POST"], @@ -12,46 +13,47 @@ export default withWinterSpec({ error: z.string().nullable().optional().default(null), }), jsonResponse: z.object({ - dev_package_example: z.object({ - dev_package_example_id: z.coerce.number(), - tscircuit_soup: z.any(), - error: z.string().nullable().optional(), - last_updated_at: z.string().datetime(), - }), + dev_package_example: DevPackageExampleSchema, }), auth: "none", })(async (req, ctx) => { + const dev_package_example = await ctx.db.get( + "dev_package_example", + req.jsonBody.dev_package_example_id + ) + + if (!dev_package_example) { + throw new NotFoundError("Package not found") + } + + const new_dev_package_example = { + ...dev_package_example, + } + + if (req.jsonBody.completed_edit_events !== undefined) { + new_dev_package_example.completed_edit_events = + req.jsonBody.completed_edit_events + new_dev_package_example.edit_events_last_updated_at = + new Date().toISOString() + } + if (req.jsonBody.edit_events_last_applied_at !== undefined) { + new_dev_package_example.edit_events_last_applied_at = + req.jsonBody.edit_events_last_applied_at + } + if (req.jsonBody.tscircuit_soup !== undefined) { + new_dev_package_example.tscircuit_soup = req.jsonBody.tscircuit_soup + new_dev_package_example.error = null + new_dev_package_example.soup_last_updated_at = new Date().toISOString() + } + if (req.jsonBody.error !== undefined) { + new_dev_package_example.error = req.jsonBody.error + } + + new_dev_package_example.last_updated_at = new Date().toISOString() + + await ctx.db.put("dev_package_example", new_dev_package_example) + return ctx.json({ - dev_package_example: await ctx.db - .updateTable("dev_package_example") - .set({ - last_updated_at: new Date().toISOString(), - }) - .$if(req.jsonBody.tscircuit_soup !== undefined, (q) => - q - .set("tscircuit_soup", req.jsonBody.tscircuit_soup) - .set("error", null) - .set("soup_last_updated_at", new Date().toISOString()) - ) - .$if(req.jsonBody.error !== undefined, (q) => - q.set("error", req.jsonBody.error) - ) - .$if(req.jsonBody.completed_edit_events !== undefined, (q) => - q - .set( - "completed_edit_events", - JSON.stringify(req.jsonBody.completed_edit_events) - ) - .set("edit_events_last_updated_at", new Date().toISOString()) - ) - .$if(req.jsonBody.edit_events_last_applied_at !== undefined, (q) => - q.set( - "edit_events_last_applied_at", - req.jsonBody.edit_events_last_applied_at! - ) - ) - .returningAll() - .where("dev_package_example_id", "=", req.jsonBody.dev_package_example_id) - .executeTakeFirstOrThrow(), + dev_package_example: new_dev_package_example, }) }) diff --git a/dev-server-api/routes/api/dev_server/reset.ts b/dev-server-api/routes/api/dev_server/reset.ts index b026a3ac..af342fb3 100644 --- a/dev-server-api/routes/api/dev_server/reset.ts +++ b/dev-server-api/routes/api/dev_server/reset.ts @@ -1,15 +1,13 @@ -import { sql } from "kysely" import { withWinterSpec } from "src/with-winter-spec" -import { z } from "zod" -import { unlinkSync } from "fs" -import { getDbFilePath } from "src/db/get-db" - -export default (req: Request) => { - unlinkSync(getDbFilePath()) +export default withWinterSpec({ + methods: ["GET", "POST"], + auth: "none", +})(async (req, ctx) => { + await ctx.db.clear() return new Response(JSON.stringify({}), { headers: { "content-type": "application/json", }, }) -} +}) diff --git a/dev-server-api/routes/api/export_files/create.ts b/dev-server-api/routes/api/export_files/create.ts index b82a77a0..7c831211 100644 --- a/dev-server-api/routes/api/export_files/create.ts +++ b/dev-server-api/routes/api/export_files/create.ts @@ -1,3 +1,4 @@ +import { ExportFileSchema } from "src/db/schema" import { publicMapExportFile } from "src/lib/public-mapping/public-map-export-file" import { export_file } from "src/lib/zod/export_file" import { withWinterSpec } from "src/with-winter-spec" @@ -8,25 +9,21 @@ export default withWinterSpec({ jsonBody: z.object({ export_request_id: z.number().int(), file_name: z.string(), - file_content_base64: z.string().transform((a) => Buffer.from(a, "base64")), + file_content_base64: z.string(), }), jsonResponse: z.object({ - export_file, + export_file: ExportFileSchema.omit({ file_content_base64: true }), }), auth: "none", })(async (req, ctx) => { - const db_export_file = await ctx.db - .insertInto("export_file") - .values({ - export_request_id: req.jsonBody.export_request_id, - file_name: req.jsonBody.file_name, - file_content: req.jsonBody.file_content_base64, - created_at: new Date().toISOString(), - }) - .returningAll() - .executeTakeFirstOrThrow() + const export_file = await ctx.db.put("export_file", { + export_request_id: req.jsonBody.export_request_id, + file_name: req.jsonBody.file_name, + file_content_base64: req.jsonBody.file_content_base64, + created_at: new Date().toISOString(), + }) return ctx.json({ - export_file: publicMapExportFile(db_export_file), + export_file, }) }) diff --git a/dev-server-api/routes/api/export_files/download.ts b/dev-server-api/routes/api/export_files/download.ts index 9b5fc1c4..d577d35a 100644 --- a/dev-server-api/routes/api/export_files/download.ts +++ b/dev-server-api/routes/api/export_files/download.ts @@ -1,6 +1,7 @@ import { publicMapExportFile } from "src/lib/public-mapping/public-map-export-file" import { export_file } from "src/lib/zod/export_file" import { withWinterSpec } from "src/with-winter-spec" +import { NotFoundError } from "winterspec/middleware" import { z } from "zod" export default withWinterSpec({ @@ -10,15 +11,17 @@ export default withWinterSpec({ }), auth: "none", })(async (req, ctx) => { - const db_export_file = await ctx.db - .selectFrom("export_file") - .selectAll() - .where("export_file_id", "=", req.query.export_file_id) - .executeTakeFirstOrThrow() + const export_file = await ctx.db.get("export_file", req.query.export_file_id) - return new Response(db_export_file.file_content, { + if (!export_file) { + throw new NotFoundError("Export file not found") + } + + const file_content = Buffer.from(export_file.file_content_base64!, "base64") + + return new Response(file_content, { headers: { - "Content-Disposition": `attachment; filename="${db_export_file.file_name}"`, + "Content-Disposition": `attachment; filename="${export_file.file_name}"`, }, }) }) diff --git a/dev-server-api/routes/api/export_requests/create.ts b/dev-server-api/routes/api/export_requests/create.ts index 7e9f2840..27d873a1 100644 --- a/dev-server-api/routes/api/export_requests/create.ts +++ b/dev-server-api/routes/api/export_requests/create.ts @@ -1,9 +1,8 @@ import { withWinterSpec } from "src/with-winter-spec" -import { NotFoundError } from "edgespec/middleware" import { z } from "zod" -import { export_request } from "src/lib/zod/export_request" import { publicMapExportRequest } from "src/lib/public-mapping/public-map-export-request" import { export_parameters } from "../../../src/lib/zod/export_parameters" +import { ExportRequestSchema } from "src/db/schema" export default withWinterSpec({ methods: ["POST"], @@ -13,23 +12,20 @@ export default withWinterSpec({ export_parameters: export_parameters, }), jsonResponse: z.object({ - export_request, + export_request: ExportRequestSchema, }), auth: "none", })(async (req, ctx) => { - const db_export_request = await ctx.db - .insertInto("export_request") - .values({ - example_file_path: req.jsonBody.example_file_path, - export_parameters: JSON.stringify(req.jsonBody.export_parameters), - export_name: req.jsonBody.export_name ?? "default", - is_complete: 0, - created_at: new Date().toISOString(), - }) - .returningAll() - .executeTakeFirstOrThrow() + const export_request = await ctx.db.put("export_request", { + example_file_path: req.jsonBody.example_file_path, + export_parameters: req.jsonBody.export_parameters, + export_name: req.jsonBody.export_name ?? "default", + is_complete: false, + has_error: false, + created_at: new Date().toISOString(), + }) return ctx.json({ - export_request: publicMapExportRequest(db_export_request), + export_request, }) }) diff --git a/dev-server-api/routes/api/export_requests/get.ts b/dev-server-api/routes/api/export_requests/get.ts index 5cc8fa55..c1697c0a 100644 --- a/dev-server-api/routes/api/export_requests/get.ts +++ b/dev-server-api/routes/api/export_requests/get.ts @@ -2,6 +2,9 @@ import { withWinterSpec } from "src/with-winter-spec" import { z } from "zod" import { export_request } from "src/lib/zod/export_request" import { publicMapExportRequest } from "src/lib/public-mapping/public-map-export-request" +import { NotFoundError } from "edgespec/middleware" +import { ExportRequestSchema } from "src/db/schema" +import { file } from "bun" export default withWinterSpec({ methods: ["GET", "POST"], @@ -9,29 +12,35 @@ export default withWinterSpec({ export_request_id: z.coerce.number(), }), jsonResponse: z.object({ - export_request: export_request, + export_request: ExportRequestSchema.extend({ + file_summary: z.array( + z.object({ file_name: z.string(), export_file_id: z.coerce.number() }) + ), + }), }), auth: "none", })(async (req, ctx) => { const { export_request_id } = req.commonParams - const db_export_request: any = await ctx.db - .selectFrom("export_request") - .selectAll() - .where("export_request_id", "=", export_request_id) - .executeTakeFirstOrThrow() + const export_request = await ctx.db.get("export_request", export_request_id) - db_export_request.file_summary = ( - await ctx.db - .selectFrom("export_file") - .where("export_request_id", "=", export_request_id) - .selectAll() - .execute() - ).map((ef) => ({ - file_name: ef.file_name, + if (!export_request) { + throw new NotFoundError("Export request not found") + } + + const ext_export_request: Parameters[0]["export_request"] = { + ...export_request, + file_summary: undefined as any, + } + + const export_files = (await ctx.db.list("export_file")).filter( + (ef) => ef.export_request_id === export_request_id + ) + ext_export_request.file_summary = export_files.map((ef) => ({ + file_name: ef.file_name!, export_file_id: ef.export_file_id, })) return ctx.json({ - export_request: publicMapExportRequest(db_export_request), + export_request: ext_export_request, }) }) diff --git a/dev-server-api/routes/api/export_requests/list.ts b/dev-server-api/routes/api/export_requests/list.ts index 99266a37..d9a8c60b 100644 --- a/dev-server-api/routes/api/export_requests/list.ts +++ b/dev-server-api/routes/api/export_requests/list.ts @@ -1,7 +1,7 @@ import { withWinterSpec } from "src/with-winter-spec" import { z } from "zod" -import { export_request } from "src/lib/zod/export_request" import { publicMapExportRequest } from "src/lib/public-mapping/public-map-export-request" +import { ExportRequestSchema } from "src/db/schema" export default withWinterSpec({ methods: ["GET", "POST"], @@ -9,20 +9,19 @@ export default withWinterSpec({ is_complete: z.boolean().nullable().default(null), }), jsonResponse: z.object({ - export_requests: z.array(export_request), + export_requests: z.array(ExportRequestSchema), }), auth: "none", })(async (req, ctx) => { const { is_complete } = req.commonParams - const db_export_requests = await ctx.db - .selectFrom("export_request") - .selectAll() - .$if(is_complete !== null, (q) => - q.where("export_request.is_complete", "=", is_complete ? 1 : 0) - ) - .execute() + const db_export_requests = await ctx.db.list("export_request") + + const filtered_requests = + is_complete === null + ? db_export_requests + : db_export_requests.filter((er) => er.is_complete === is_complete) return ctx.json({ - export_requests: db_export_requests.map((er) => publicMapExportRequest(er)), + export_requests: filtered_requests, }) }) diff --git a/dev-server-api/routes/api/export_requests/update.ts b/dev-server-api/routes/api/export_requests/update.ts index f2ebeaf0..8bbfde07 100644 --- a/dev-server-api/routes/api/export_requests/update.ts +++ b/dev-server-api/routes/api/export_requests/update.ts @@ -3,7 +3,7 @@ import { NotFoundError } from "edgespec/middleware" import { z } from "zod" import { export_request } from "src/lib/zod/export_request" import { publicMapExportRequest } from "src/lib/public-mapping/public-map-export-request" -import { export_parameters } from "src/lib/zod/export_parameters" +import { ExportRequestSchema } from "src/db/schema" export default withWinterSpec({ methods: ["POST"], @@ -13,24 +13,24 @@ export default withWinterSpec({ has_error: z.boolean().optional(), error: z.string().optional(), }), - jsonResponse: z.object({}), + jsonResponse: z.object({ + export_request: ExportRequestSchema, + }), auth: "none", })(async (req, ctx) => { - await ctx.db - .updateTable("export_request") - .$if(req.jsonBody.is_complete !== undefined, (qb) => - qb.set({ - is_complete: req.jsonBody.is_complete ? 1 : 0, - }) - ) - .$if(req.jsonBody.has_error !== undefined, (qb) => - qb.set({ - has_error: req.jsonBody.has_error ? 1 : 0, - error: req.jsonBody.error, - }) - ) - .where("export_request_id", "=", req.jsonBody.export_request_id) - .execute() + const { export_request_id, ...updateData } = req.jsonBody + const existingRequest = await ctx.db.get("export_request", export_request_id) + + if (!existingRequest) { + throw new NotFoundError("Export request not found") + } + + const updatedRequest = await ctx.db.put("export_request", { + ...existingRequest, + ...updateData, + }) - return ctx.json({}) + return ctx.json({ + export_request: updatedRequest, + }) }) diff --git a/dev-server-api/routes/api/package_info/create.ts b/dev-server-api/routes/api/package_info/create.ts index 39d56457..4e85381e 100644 --- a/dev-server-api/routes/api/package_info/create.ts +++ b/dev-server-api/routes/api/package_info/create.ts @@ -4,25 +4,23 @@ import { z } from "zod" export default withWinterSpec({ methods: ["POST"], jsonBody: z.object({ - package_name: z.string() + package_name: z.string(), }), jsonResponse: z.object({ package_info: z.object({ - name: z.string() - }) + name: z.string(), + }), }), auth: "none", })(async (req, ctx) => { const package_name = req.jsonBody.package_name - const package_info = await ctx.db - .insertInto("package_info") - .values({ - name: package_name - }) - .returningAll() - .executeTakeFirstOrThrow() + + const package_info = await ctx.db.put("package_info", { + package_info_id: 1, + name: package_name, + }) return ctx.json({ - package_info + package_info, }) }) diff --git a/dev-server-api/routes/api/package_info/get.ts b/dev-server-api/routes/api/package_info/get.ts index 207876aa..72fc2e68 100644 --- a/dev-server-api/routes/api/package_info/get.ts +++ b/dev-server-api/routes/api/package_info/get.ts @@ -1,18 +1,15 @@ -import { export_package_info } from "src/lib/zod/export_package_info" +import { PackageInfoSchema } from "src/db/schema" import { withWinterSpec } from "src/with-winter-spec" import { z } from "zod" export default withWinterSpec({ methods: ["GET"], jsonResponse: z.object({ - package_info: export_package_info + package_info: PackageInfoSchema, }), auth: "none", })(async (req, ctx) => { - const package_info = await ctx.db - .selectFrom("package_info") - .select("name") - .executeTakeFirstOrThrow() + const package_info = await ctx.db.get("package_info", 1) - return ctx.json({ package_info }) -}) \ No newline at end of file + return ctx.json({ package_info: package_info! }) +}) diff --git a/dev-server-api/routes/index.ts b/dev-server-api/routes/index.ts new file mode 100644 index 00000000..4caf598f --- /dev/null +++ b/dev-server-api/routes/index.ts @@ -0,0 +1,16 @@ +import { withWinterSpec } from "../src/with-winter-spec" +import { z } from "zod" + +export default withWinterSpec({ + methods: ["GET"], + auth: "none", +})(async (req, ctx) => { + return new Response( + `This is the dev server API view database`, + { + headers: { + "content-type": "text/html", + }, + } + ) +}) diff --git a/dev-server-api/server.ts b/dev-server-api/server.ts index 0059b716..7db77be5 100644 --- a/dev-server-api/server.ts +++ b/dev-server-api/server.ts @@ -6,7 +6,7 @@ const serverFetch = await createFetchHandlerFromDir( join(import.meta.dir, "./routes") ) -console.log("starting dev-server-api on localhost:3021") +console.log("starting dev-server-api on http://localhost:3021") Bun.serve({ fetch: (bunReq) => { const req = new EdgeRuntimeRequest(bunReq.url, { diff --git a/dev-server-api/src/db/get-db.ts b/dev-server-api/src/db/get-db.ts index bf015bbd..3d9da27b 100644 --- a/dev-server-api/src/db/get-db.ts +++ b/dev-server-api/src/db/get-db.ts @@ -1,117 +1,26 @@ import { mkdirSync } from "fs" -import { Kysely, SqliteDialect, sql, type Generated } from "kysely" import * as Path from "path" -import { createSchema } from "./create-schema" +import { ZodLevelDatabase } from "./zod-level-db" -export interface PackageInfo { - name: string -} - -export interface DevPackageExample { - dev_package_example_id: Generated - tscircuit_soup: any - completed_edit_events: any - file_path: string - export_name: string - error: string | null - is_loading: 1 | 0 - last_updated_at: string - soup_last_updated_at: string - edit_events_last_updated_at: string - edit_events_last_applied_at: string -} - -export interface ExportRequest { - export_request_id: Generated - example_file_path: string - export_parameters: string - export_name: string - is_complete: 1 | 0 - has_error: 1 | 0 - error?: string - created_at: string -} - -export interface ExportFile { - export_file_id: Generated - file_name: string - file_content: Buffer - export_request_id: number - created_at: string -} - -interface KyselyDatabaseSchema { - dev_package_example: DevPackageExample - export_request: ExportRequest - export_file: ExportFile - package_info: PackageInfo -} - -export type DbClient = Kysely - -// let globalDb: Database | undefined - -let globalDb: Kysely | undefined +let globalDb: ZodLevelDatabase | undefined export const getDbFilePath = () => - process.env.TSCI_DEV_SERVER_DB ?? "./.tscircuit/dev-server.sqlite" + process.env.TSCI_DEV_SERVER_DB ?? "./.tscircuit/devdb" -export const getDb = async (): Promise> => { - if (globalDb) return globalDb - - const devServerDbPath = getDbFilePath() - - mkdirSync(Path.dirname(devServerDbPath), { recursive: true }) - - // better-sqlite3 doesn't work in bun, so if we see we can use the bun - // alternative, attempt to use that instead - let dialect: any - - if (typeof Bun !== "undefined") { - // console.log("Attempting to use bun-sqlite") - try { - const { BunSqliteDialect } = await import("kysely-bun-sqlite") - const { Database } = await import("bun:sqlite") - dialect = new BunSqliteDialect({ - database: new Database(devServerDbPath, { - create: true, - }), - }) - } catch (e) { } - } - - if (!dialect) { - // console.log("Attempting to use better-sqlite3") - try { - const BetterSqlite3 = await import("better-sqlite3") - dialect = new SqliteDialect({ - database: new BetterSqlite3.default(devServerDbPath), - }) - } catch (e) { } - } - - if (!dialect) { - throw new Error("Was not able to load sqlite dialect") +export const getDb = async (): Promise => { + if (globalDb) { + return globalDb } - const db = new Kysely({ - dialect, - }) + const devServerDbPath = getDbFilePath() - await sql`pragma busy_timeout = 5000`.execute(db) + mkdirSync(devServerDbPath, { recursive: true }) - const schemaExistsResult = await sql` - SELECT name - FROM sqlite_master - WHERE type='table' AND name IN ('dev_package_example', 'export_request', 'export_file', 'package_info') - `.execute(db) + const db = new ZodLevelDatabase(devServerDbPath) - // Check if the number of existing tables matches the number of required tables - if (schemaExistsResult.rows.length < 4) { - await createSchema(db) - } + db.open() globalDb = db return db -} \ No newline at end of file +} diff --git a/dev-server-api/src/db/schema.ts b/dev-server-api/src/db/schema.ts index f0079d3e..cadd7091 100644 --- a/dev-server-api/src/db/schema.ts +++ b/dev-server-api/src/db/schema.ts @@ -1,21 +1,22 @@ import { z } from "zod" // Helper function for nullable fields -const nullableText = () => z.string().nullable() +const nullableText = () => z.string().nullable().default(null) +const id = () => z.any().pipe(z.number().int()) // PackageInfo schema export const PackageInfoSchema = z.object({ - package_info_id: z.number().int(), + package_info_id: id(), name: z.string(), }) // DevPackageExample schema export const DevPackageExampleSchema = z.object({ - dev_package_example_id: z.number().int(), + dev_package_example_id: id(), file_path: z.string(), export_name: nullableText(), tscircuit_soup: z.any().nullable(), // Using any for JSON type - completed_edit_events: z.any().nullable(), // Using any for JSON type + completed_edit_events: z.array(z.any()).default([]), // Using any for JSON type error: nullableText(), is_loading: z.boolean(), soup_last_updated_at: nullableText(), @@ -26,7 +27,7 @@ export const DevPackageExampleSchema = z.object({ // ExportRequest schema export const ExportRequestSchema = z.object({ - export_request_id: z.number().int(), + export_request_id: id(), example_file_path: nullableText(), export_parameters: z.any().nullable(), // Using any for JSON type export_name: nullableText(), @@ -38,10 +39,9 @@ export const ExportRequestSchema = z.object({ // ExportFile schema export const ExportFileSchema = z.object({ - export_file_id: z.number().int(), + export_file_id: id(), file_name: nullableText(), - file_content: z.instanceof(Buffer).nullable(), // For BLOB type - is_complete: z.boolean(), + file_content_base64: z.string().nullable(), export_request_id: z.number().int().nullable(), created_at: nullableText(), }) @@ -56,6 +56,7 @@ export const DBSchema = z.object({ // TypeScript type inference export type DBSchemaType = z.infer +export type DBInputSchemaType = z.input // You can also export individual types if needed export type PackageInfo = z.infer diff --git a/dev-server-api/src/db/level-db.ts b/dev-server-api/src/db/zod-level-db.ts similarity index 62% rename from dev-server-api/src/db/level-db.ts rename to dev-server-api/src/db/zod-level-db.ts index 7a363141..b3a94191 100644 --- a/dev-server-api/src/db/level-db.ts +++ b/dev-server-api/src/db/zod-level-db.ts @@ -1,6 +1,6 @@ import { Level } from "level" import { z } from "zod" -import { DBSchema, type DBSchemaType } from "./schema" +import { DBSchema, type DBSchemaType, type DBInputSchemaType } from "./schema" // Create a wrapper class for Level with Zod validation export class ZodLevelDatabase { @@ -10,10 +10,18 @@ export class ZodLevelDatabase { this.db = new Level(location) } + async open() { + return this.db.open() + } + + async close() { + return this.db.close() + } + async get( collection: K, - id: string - ): Promise { + id: string | number + ): Promise { const key = `${collection}:${id}` const data = await this.db.get(key) return DBSchema.shape[collection].parse(JSON.parse(data)) as any @@ -21,20 +29,22 @@ export class ZodLevelDatabase { async put( collection: K, - value: DBSchemaType[K] - ): Promise { + value: DBInputSchemaType[K] + ): Promise { const idkey = `${collection}_id` const valueLoose: any = value if (!valueLoose[idkey]) { // generate an id using the "count" key - let count = await this.db.get(`${collection}:count`) - if (!count) count = 1 + let count = await this.db + .get(`${collection}.count`, { valueEncoding: "json" }) + .catch(() => 1) ;(value as any)[idkey] = count - await this.db.put(`${collection}:count`, count + 1) + await this.db.put(`${collection}.count`, count + 1) } const key = `${collection}:${valueLoose[idkey]}` const validatedData = DBSchema.shape[collection].parse(value) await this.db.put(key, JSON.stringify(validatedData)) + return validatedData as DBSchemaType[K] } async del( @@ -49,7 +59,6 @@ export class ZodLevelDatabase { collection: K, partialObject: Partial ): Promise { - const results: DBSchemaType[K][] = [] const schema = DBSchema.shape[collection] for await (const [key, value] of this.db.iterator({ @@ -95,4 +104,43 @@ export class ZodLevelDatabase { } return true } + + async dump(): Promise { + // Serialize all data in the database + const dump: any = {} + for await (const [key, value] of this.db.iterator({})) { + const [collection, id] = key.split(":") + if (!dump[collection]) { + dump[collection] = {} + } + dump[collection][id] = JSON.parse(value) + } + return dump + } + + async list( + collection: K + ): Promise { + const schema = DBSchema.shape[collection] + const results: DBSchemaType[K][] = [] + + for await (const [key, value] of this.db.iterator({ + gte: `${collection}:`, + lte: `${collection}:\uffff`, + })) { + if (key.endsWith(".count")) continue + try { + const parsedValue = schema.parse(JSON.parse(value)) + results.push(parsedValue as DBSchemaType[K]) + } catch (error) { + console.error(`Error parsing value for key ${key}:`, error) + } + } + + return results + } + + async clear() { + return this.db.clear() + } } diff --git a/dev-server-api/src/lib/zod/export_file.ts b/dev-server-api/src/lib/zod/export_file.ts deleted file mode 100644 index 5c913ece..00000000 --- a/dev-server-api/src/lib/zod/export_file.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { z } from "zod" - -export const export_file = z.object({ - export_file_id: z.number().int(), - export_request_id: z.number().int(), - file_name: z.string(), - created_at: z.string(), -}) diff --git a/dev-server-api/src/lib/zod/export_package_info.ts b/dev-server-api/src/lib/zod/export_package_info.ts deleted file mode 100644 index cf750c35..00000000 --- a/dev-server-api/src/lib/zod/export_package_info.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { z } from "zod" - -export const export_package_info = z.object({ - name: z.string() -}) \ No newline at end of file diff --git a/dev-server-api/src/lib/zod/export_request.ts b/dev-server-api/src/lib/zod/export_request.ts deleted file mode 100644 index dc19e97e..00000000 --- a/dev-server-api/src/lib/zod/export_request.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { z } from "zod" -import { export_parameters } from "./export_parameters" - -export const export_request = z.object({ - export_request_id: z.coerce.number(), - is_complete: z.boolean(), - created_at: z.string(), - export_name: z.string(), - example_file_path: z.string(), - export_parameters, - file_summary: z - .array( - z.object({ - file_name: z.string(), - export_file_id: z.number().int(), - }) - ) - .optional(), -}) - -export type ExportRequest = z.infer diff --git a/dev-server-api/src/middlewares/with-db.ts b/dev-server-api/src/middlewares/with-db.ts index af4f736f..c07474e5 100644 --- a/dev-server-api/src/middlewares/with-db.ts +++ b/dev-server-api/src/middlewares/with-db.ts @@ -1,14 +1,18 @@ import type { Middleware } from "winterspec" -import { getDb, type DbClient } from "../db/get-db" +import { getDb } from "../db/get-db" +import type { ZodLevelDatabase } from "src/db/zod-level-db" export const withDb: Middleware< {}, { - db: DbClient + db: ZodLevelDatabase } > = async (req, ctx, next) => { if (!ctx.db) { ctx.db = await getDb() } - return next(req, ctx) + // await ctx.db.open() + const res = await next(req, ctx) + // await ctx.db.close() + return res } diff --git a/dev-server-api/static-routes.ts b/dev-server-api/static-routes.ts index 3c363d4c..a5868452 100644 --- a/dev-server-api/static-routes.ts +++ b/dev-server-api/static-routes.ts @@ -2,6 +2,7 @@ // import { WinterSpecRouteMap } from "@winterspec/types" const routeMap = { + "/api/db/download": (await import('routes/api/db/download.ts')).default, "/api/dev_package_examples/create": (await import('routes/api/dev_package_examples/create.ts')).default, "/api/dev_package_examples/get": (await import('routes/api/dev_package_examples/get.ts')).default, "/api/dev_package_examples/list": (await import('routes/api/dev_package_examples/list.ts')).default, @@ -14,7 +15,10 @@ const routeMap = { "/api/export_requests/list": (await import('routes/api/export_requests/list.ts')).default, "/api/export_requests/update": (await import('routes/api/export_requests/update.ts')).default, "/api/health": (await import('routes/api/health.ts')).default, - "/health": (await import('routes/health.ts')).default + "/api/package_info/create": (await import('routes/api/package_info/create.ts')).default, + "/api/package_info/get": (await import('routes/api/package_info/get.ts')).default, + "/health": (await import('routes/health.ts')).default, + "/": (await import('routes/index.ts')).default } export default routeMap diff --git a/dev-server-api/tests/fixtures/get-test-server.ts b/dev-server-api/tests/fixtures/get-test-server.ts index 41870d61..a28887ad 100644 --- a/dev-server-api/tests/fixtures/get-test-server.ts +++ b/dev-server-api/tests/fixtures/get-test-server.ts @@ -1,6 +1,7 @@ import { afterEach } from "bun:test" import defaultAxios from "redaxios" import { startServer } from "./start-server" +import { tmpdir } from "node:os" interface TestFixture { url: string @@ -9,6 +10,7 @@ interface TestFixture { } export const getTestFixture = async (): Promise => { + process.env.TSCI_DEV_SERVER_DB = tmpdir() + `/${Math.random()}` + "/devdb" const port = 3001 + Math.floor(Math.random() * 999) const server = startServer({ port }) const url = `http://localhost:${port}` @@ -17,7 +19,6 @@ export const getTestFixture = async (): Promise => { }) afterEach(() => { - console.log("closing server") server.stop() }) diff --git a/dev-server-api/tests/routes/dev_package_examples/create.test.ts b/dev-server-api/tests/routes/dev_package_examples/create.test.ts new file mode 100644 index 00000000..d402d7a4 --- /dev/null +++ b/dev-server-api/tests/routes/dev_package_examples/create.test.ts @@ -0,0 +1,19 @@ +import { it, expect } from "bun:test" +import { getTestFixture } from "tests/fixtures/get-test-server" + +it("POST /api/dev_package_examples/create", async () => { + const { axios } = await getTestFixture() + + const res = await axios + .post("/api/dev_package_examples/create", { + file_path: "examples/basic-resistor.tsx", + export_name: "default", + tscircuit_soup: [], + is_loading: true, + }) + .then((r) => r.data) + + expect(res.dev_package_example.file_path).toEqual( + "examples/basic-resistor.tsx" + ) +}) diff --git a/dev-server-api/tests/routes/dev_package_examples/get.test.ts b/dev-server-api/tests/routes/dev_package_examples/get.test.ts new file mode 100644 index 00000000..c551a845 --- /dev/null +++ b/dev-server-api/tests/routes/dev_package_examples/get.test.ts @@ -0,0 +1,25 @@ +import { it, expect } from "bun:test" +import { getTestFixture } from "tests/fixtures/get-test-server" + +it("POST /api/dev_package_examples/create", async () => { + const { axios } = await getTestFixture() + + await axios + .post("/api/dev_package_examples/create", { + file_path: "examples/basic-resistor.tsx", + export_name: "default", + tscircuit_soup: [], + is_loading: true, + }) + .then((r) => r.data) + + const res = await axios + .post("/api/dev_package_examples/get", { + dev_package_example_id: 1, + }) + .then((r) => r.data) + + expect(res.dev_package_example.file_path).toEqual( + "examples/basic-resistor.tsx" + ) +}) diff --git a/dev-server-api/tests/routes/dev_package_examples/list.test.ts b/dev-server-api/tests/routes/dev_package_examples/list.test.ts new file mode 100644 index 00000000..35c59981 --- /dev/null +++ b/dev-server-api/tests/routes/dev_package_examples/list.test.ts @@ -0,0 +1,32 @@ +import { it, expect } from "bun:test" +import { getTestFixture } from "tests/fixtures/get-test-server" + +it("GET /api/dev_package_examples/list", async () => { + const { axios } = await getTestFixture() + + // First, create a dev package example + await axios.post("/api/dev_package_examples/create", { + file_path: "examples/test-example.tsx", + export_name: "default", + tscircuit_soup: [], + is_loading: false, + }) + + // Then, list all dev package examples + const res = await axios + .post("/api/dev_package_examples/list") + .then((r) => r.data) + + expect(res.dev_package_examples).toBeDefined() + expect(Array.isArray(res.dev_package_examples)).toBe(true) + expect(res.dev_package_examples.length).toBeGreaterThan(0) + + const example = res.dev_package_examples.find( + (e: any) => e.file_path === "examples/test-example.tsx" + ) + expect(example).toBeDefined() + expect(example.export_name).toBe("default") + expect(example.is_loading).toBe(false) + expect(example.dev_package_example_id).toBeDefined() + expect(example.last_updated_at).toBeDefined() +}) diff --git a/dev-server-api/tests/routes/dev_package_examples/update.test.ts b/dev-server-api/tests/routes/dev_package_examples/update.test.ts new file mode 100644 index 00000000..829ae39f --- /dev/null +++ b/dev-server-api/tests/routes/dev_package_examples/update.test.ts @@ -0,0 +1,28 @@ +import { it, expect } from "bun:test" +import { getTestFixture } from "tests/fixtures/get-test-server" + +it("POST /api/dev_package_examples/update", async () => { + const { axios } = await getTestFixture() + + await axios + .post("/api/dev_package_examples/create", { + file_path: "examples/basic-resistor.tsx", + export_name: "default", + tscircuit_soup: [], + is_loading: true, + }) + .then((r) => r.data) + + const res = await axios + .post("/api/dev_package_examples/update", { + dev_package_example_id: 1, + completed_edit_events: [], + edit_events_last_applied_at: "2023-01-01T00:00:00.000Z", + }) + .then((r) => r.data) + + expect(res.dev_package_example.completed_edit_events).toEqual([]) + expect(res.dev_package_example.edit_events_last_applied_at).toEqual( + "2023-01-01T00:00:00.000Z" + ) +}) diff --git a/dev-server-api/tests/routes/export_files/create.test.ts b/dev-server-api/tests/routes/export_files/create.test.ts new file mode 100644 index 00000000..44c026a2 --- /dev/null +++ b/dev-server-api/tests/routes/export_files/create.test.ts @@ -0,0 +1,18 @@ +import { it, expect } from "bun:test" +import { getTestFixture } from "tests/fixtures/get-test-server" + +it("POST /api/export_files/create", async () => { + const { axios } = await getTestFixture() + + const res = await axios + .post("/api/export_files/create", { + export_request_id: 1, + file_name: "test.png", + file_content_base64: + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==", + }) + .then((r) => r.data) + + expect(res.export_file.export_request_id).toEqual(1) + expect(res.export_file.file_name).toEqual("test.png") +}) diff --git a/dev-server-api/tests/routes/export_files/download.test.ts b/dev-server-api/tests/routes/export_files/download.test.ts new file mode 100644 index 00000000..d4590169 --- /dev/null +++ b/dev-server-api/tests/routes/export_files/download.test.ts @@ -0,0 +1,29 @@ +import { it, expect } from "bun:test" +import { getTestFixture } from "tests/fixtures/get-test-server" + +it("GET /api/export_files/download", async () => { + const { axios } = await getTestFixture() + + const exampleBase64 = Buffer.from("example").toString("base64") + + const res = await axios + .post("/api/export_files/create", { + export_request_id: 1, + file_name: "test.png", + file_content_base64: exampleBase64, + }) + .then((r) => r.data) + + const downloadRes = await axios + .get( + `/api/export_files/download?export_file_id=${res.export_file.export_file_id}` + ) + .then((r) => r.data) + + // Convert downloadRes to base64 string + const downloadResBase64 = Buffer.from(downloadRes, "binary").toString( + "base64" + ) + + expect(downloadResBase64).toEqual(exampleBase64) +}) diff --git a/dev-server-api/tests/routes/export_requests/create.test.ts b/dev-server-api/tests/routes/export_requests/create.test.ts new file mode 100644 index 00000000..c3048148 --- /dev/null +++ b/dev-server-api/tests/routes/export_requests/create.test.ts @@ -0,0 +1,24 @@ +import { it, expect } from "bun:test" +import { getTestFixture } from "tests/fixtures/get-test-server" + +it("POST /api/export_requests/create", async () => { + const { axios } = await getTestFixture() + + const res = await axios.post("/api/export_requests/create", { + example_file_path: "examples/test-example.tsx", + export_name: "default", + export_parameters: { should_export_gerber_zip: true }, + }) + + expect(res.status).toBe(200) + expect(res.data.export_request).toBeDefined() + expect(res.data.export_request.example_file_path).toBe( + "examples/test-example.tsx" + ) + expect(res.data.export_request.export_name).toBe("default") + expect(res.data.export_request.export_parameters).toMatchObject({ + should_export_gerber_zip: true, + }) + expect(res.data.export_request.is_complete).toBe(false) + expect(res.data.export_request.has_error).toBe(false) +}) diff --git a/dev-server-api/tests/routes/export_requests/get.test.ts b/dev-server-api/tests/routes/export_requests/get.test.ts new file mode 100644 index 00000000..b6478722 --- /dev/null +++ b/dev-server-api/tests/routes/export_requests/get.test.ts @@ -0,0 +1,41 @@ +import { it, expect } from "bun:test" +import { getTestFixture } from "tests/fixtures/get-test-server" + +it("GET /api/export_requests/get", async () => { + const { axios } = await getTestFixture() + + // First, create an export request + const createRes = await axios.post("/api/export_requests/create", { + example_file_path: "examples/test-example.tsx", + export_name: "default", + export_parameters: { should_export_gerber_zip: true }, + }) + + const exportRequestId = createRes.data.export_request.export_request_id + + // Now, get the export request + const getRes = await axios.post(`/api/export_requests/get`, { + export_request_id: exportRequestId, + }) + + expect(getRes.status).toBe(200) + expect(getRes.data.export_request).toBeDefined() + expect(getRes.data.export_request.export_request_id).toBe(exportRequestId) + expect(getRes.data.export_request.example_file_path).toBe( + "examples/test-example.tsx" + ) + expect(getRes.data.export_request.export_name).toBe("default") + expect(getRes.data.export_request.export_parameters).toMatchObject({ + should_export_gerber_zip: true, + }) +}) + +it("GET /api/export_requests/get - Not Found", async () => { + const { axios } = await getTestFixture() + + try { + await axios.post("/api/export_requests/get", { export_request_id: 999999 }) + } catch (error: any) { + expect(error.status).toBe(404) + } +}) diff --git a/dev-server-api/tests/routes/export_requests/list.test.ts b/dev-server-api/tests/routes/export_requests/list.test.ts new file mode 100644 index 00000000..f330f135 --- /dev/null +++ b/dev-server-api/tests/routes/export_requests/list.test.ts @@ -0,0 +1,35 @@ +import { it, expect } from "bun:test" +import { getTestFixture } from "tests/fixtures/get-test-server" + +it("GET /api/export_requests/list", async () => { + const { axios } = await getTestFixture() + + // Create two export requests + await axios.post("/api/export_requests/create", { + example_file_path: "examples/test-example1.tsx", + export_name: "default", + export_parameters: { format: "gerber" }, + }) + + await axios.post("/api/export_requests/create", { + example_file_path: "examples/test-example2.tsx", + export_name: "default", + export_parameters: { format: "svg" }, + }) + + // List all export requests + const listRes = await axios.post("/api/export_requests/list", {}) + + expect(listRes.status).toBe(200) + expect(listRes.data.export_requests).toBeDefined() + expect(Array.isArray(listRes.data.export_requests)).toBe(true) + + // List completed export requests (should be empty) + const completedRes = await axios.post("/api/export_requests/list", { + is_complete: true, + }) + + expect(completedRes.status).toBe(200) + expect(completedRes.data.export_requests).toBeDefined() + expect(Array.isArray(completedRes.data.export_requests)).toBe(true) +}) diff --git a/dev-server-api/tests/routes/export_requests/update.test.ts b/dev-server-api/tests/routes/export_requests/update.test.ts new file mode 100644 index 00000000..762524e8 --- /dev/null +++ b/dev-server-api/tests/routes/export_requests/update.test.ts @@ -0,0 +1,50 @@ +import { it, expect } from "bun:test" +import { getTestFixture } from "tests/fixtures/get-test-server" + +it("POST /api/export_requests/update", async () => { + const { axios } = await getTestFixture() + + // First, create an export request + const createRes = await axios.post("/api/export_requests/create", { + example_file_path: "examples/test-example.tsx", + export_name: "default", + export_parameters: { format: "gerber", should_export_gerber_zip: true }, + }) + + const exportRequestId = createRes.data.export_request.export_request_id + + // Now, update the export request + const updateRes = await axios.post("/api/export_requests/update", { + export_request_id: exportRequestId, + is_complete: true, + has_error: false, + }) + + expect(updateRes.status).toBe(200) + expect(updateRes.data.export_request).toBeDefined() + expect(updateRes.data.export_request.export_request_id).toBe(exportRequestId) + expect(updateRes.data.export_request.is_complete).toBe(true) + expect(updateRes.data.export_request.has_error).toBe(false) + + // // Verify the update + const getRes = await axios.post(`/api/export_requests/get`, { + export_request_id: exportRequestId, + }) + + expect(getRes.status).toBe(200) + expect(getRes.data.export_request.is_complete).toBe(true) + expect(getRes.data.export_request.has_error).toBe(false) +}) + +it("POST /api/export_requests/update - Not Found", async () => { + const { axios } = await getTestFixture() + + try { + await axios.post("/api/export_requests/update", { + export_request_id: 999999, + is_complete: true, + }) + } catch (error: any) { + expect(error.status).toBe(404) + } +}) diff --git a/lib/cmd-fns/dev/index.ts b/lib/cmd-fns/dev/index.ts index 8dc14926..16c8ae9e 100644 --- a/lib/cmd-fns/dev/index.ts +++ b/lib/cmd-fns/dev/index.ts @@ -1,5 +1,5 @@ import $ from "dax-sh" -import fs from 'fs' +import fs from "fs" import { unlink } from "fs/promises" import kleur from "kleur" import open from "open" @@ -58,7 +58,7 @@ export const devCmd = async (ctx: AppContext, args: any) => { // TODO // Delete old .tscircuit/dev-server.sqlite - unlink(Path.join(cwd, ".tscircuit/dev-server.sqlite")).catch(() => { }) + // unlink(Path.join(cwd, ".tscircuit/dev-server.sqlite")).catch(() => { }) console.log( kleur.green( @@ -71,20 +71,23 @@ export const devCmd = async (ctx: AppContext, args: any) => { const server = await startDevServer({ port, devServerAxios }) // Reset the database, allows migration to re-run - await devServerAxios.post("/api/dev_server/reset") - .catch(e => { - console.log("Failed to reset database, continuing anyway...") - }) + await devServerAxios.post("/api/dev_server/reset").catch((e) => { + console.log("Failed to reset database, continuing anyway...") + }) // Add package name to the package_info table - const packageJsonPath = Path.resolve(cwd, 'package.json') - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) + const packageJsonPath = Path.resolve(cwd, "package.json") + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) const packageName = packageJson.name console.log(`Adding package info...`) - await devServerAxios.post("/api/package_info/create", { - package_name: packageName - }, ctx) + await devServerAxios.post( + "/api/package_info/create", + { + package_name: packageName, + }, + ctx + ) // Soupify all examples console.log(`Loading examples...`) diff --git a/package.json b/package.json index aeb88975..78da044a 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "bootstrap": "bun i && cd dev-server-api && bun i && cd ../dev-server-frontend && bun i", "bootstrap:ci": "bun i --frozen-lockfile && cd dev-server-api && bun i --frozen-lockfile && cd ../dev-server-frontend && bun i --frozen-lockfile", "start": "bun cli.ts", - "dev": "TSCI_DEV_SERVER_DB=$(pwd)/.tscircuit/dev-server.sqlite concurrently 'cd dev-server-api && bun run build && bun start' 'cd dev-server-frontend && bun start' 'bun run dev-with-test-project'", + "dev": "TSCI_DEV_SERVER_DB=$(pwd)/.tscircuit/devdb concurrently 'cd dev-server-api && bun run build && bun start' 'cd dev-server-frontend && bun start' 'bun run dev-with-test-project'", "clear": "rm -rf .tscircuit ./dev-server-api/.edgespec", "start:dev-server": "bun build:dev-server && bun cli.ts dev -y --cwd ./tests/assets/example-project", "build:dev-server": "cd dev-server-api && bun run build && cd ../dev-server-frontend && bun run build", @@ -37,7 +37,6 @@ "@hono/node-server": "^1.8.2", "archiver": "^7.0.1", "axios": "^1.6.7", - "better-sqlite3": "^11.1.2", "chokidar": "^3.6.0", "commander": "^12.0.0", "configstore": "^6.0.0", @@ -53,7 +52,6 @@ "ignore": "^5.3.1", "json5": "^2.2.3", "kleur": "^4.1.5", - "kysely-bun-sqlite": "^0.3.2", "lodash": "^4.17.21", "mime-types": "^2.1.35", "minimist": "^1.2.8",