From b960dbabb667c772e138db2a616c9070cc82a22f Mon Sep 17 00:00:00 2001 From: Diego Fernandes Date: Thu, 1 Feb 2024 15:34:34 -0300 Subject: [PATCH 01/21] chore: create companies table --- .../migration.sql | 14 ++++++++ .../migration.sql | 25 ++++++++++++++ .../migration.sql | 34 +++++++++++++++++++ prisma/schema.prisma | 20 +++++++++-- 4 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 prisma/migrations/20240201182601_create_companies/migration.sql create mode 100644 prisma/migrations/20240201182915_add_company_id_reference_to_tables/migration.sql create mode 100644 prisma/migrations/20240201183144_set_company_id_as_not_null/migration.sql diff --git a/prisma/migrations/20240201182601_create_companies/migration.sql b/prisma/migrations/20240201182601_create_companies/migration.sql new file mode 100644 index 0000000..3338185 --- /dev/null +++ b/prisma/migrations/20240201182601_create_companies/migration.sql @@ -0,0 +1,14 @@ +-- CreateTable +CREATE TABLE "Company" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "domain" TEXT NOT NULL, + + CONSTRAINT "Company_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Company_domain_key" ON "Company"("domain"); + +INSERT INTO "Company" ("id", "name", "domain") +VALUES ('BE7CB85C-E3A4-4B37-908C-400C1A582749', 'Rocketseat', 'rocketseat.team'); \ No newline at end of file diff --git a/prisma/migrations/20240201182915_add_company_id_reference_to_tables/migration.sql b/prisma/migrations/20240201182915_add_company_id_reference_to_tables/migration.sql new file mode 100644 index 0000000..e3e93f5 --- /dev/null +++ b/prisma/migrations/20240201182915_add_company_id_reference_to_tables/migration.sql @@ -0,0 +1,25 @@ +-- AlterTable +ALTER TABLE "Tag" ADD COLUMN "companyId" TEXT; + +-- AlterTable +ALTER TABLE "UploadBatch" ADD COLUMN "companyId" TEXT; + +-- AlterTable +ALTER TABLE "Video" ADD COLUMN "companyId" TEXT; + +-- AddForeignKey +ALTER TABLE "Video" ADD CONSTRAINT "Video_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UploadBatch" ADD CONSTRAINT "UploadBatch_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Tag" ADD CONSTRAINT "Tag_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- Insert default values + +UPDATE "Tag" SET "companyId" = 'BE7CB85C-E3A4-4B37-908C-400C1A582749' WHERE "companyId" IS NULL; + +UPDATE "UploadBatch" SET "companyId" = 'BE7CB85C-E3A4-4B37-908C-400C1A582749' WHERE "companyId" IS NULL; + +UPDATE "Video" SET "companyId" = 'BE7CB85C-E3A4-4B37-908C-400C1A582749' WHERE "companyId" IS NULL; \ No newline at end of file diff --git a/prisma/migrations/20240201183144_set_company_id_as_not_null/migration.sql b/prisma/migrations/20240201183144_set_company_id_as_not_null/migration.sql new file mode 100644 index 0000000..d86a96c --- /dev/null +++ b/prisma/migrations/20240201183144_set_company_id_as_not_null/migration.sql @@ -0,0 +1,34 @@ +/* + Warnings: + + - Made the column `companyId` on table `Tag` required. This step will fail if there are existing NULL values in that column. + - Made the column `companyId` on table `UploadBatch` required. This step will fail if there are existing NULL values in that column. + - Made the column `companyId` on table `Video` required. This step will fail if there are existing NULL values in that column. + +*/ +-- DropForeignKey +ALTER TABLE "Tag" DROP CONSTRAINT "Tag_companyId_fkey"; + +-- DropForeignKey +ALTER TABLE "UploadBatch" DROP CONSTRAINT "UploadBatch_companyId_fkey"; + +-- DropForeignKey +ALTER TABLE "Video" DROP CONSTRAINT "Video_companyId_fkey"; + +-- AlterTable +ALTER TABLE "Tag" ALTER COLUMN "companyId" SET NOT NULL; + +-- AlterTable +ALTER TABLE "UploadBatch" ALTER COLUMN "companyId" SET NOT NULL; + +-- AlterTable +ALTER TABLE "Video" ALTER COLUMN "companyId" SET NOT NULL; + +-- AddForeignKey +ALTER TABLE "Video" ADD CONSTRAINT "Video_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UploadBatch" ADD CONSTRAINT "UploadBatch_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Tag" ADD CONSTRAINT "Tag_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3780cd3..1042b7e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -10,9 +10,20 @@ datasource db { directUrl = env("DIRECT_DATABASE_URL") } +model Company { + id String @id @default(uuid()) + name String + domain String @unique + + videos Video[] + tags Tag[] + batches UploadBatch[] +} + model Video { id String @id @default(uuid()) uploadBatchId String? + companyId String language String @default("pt") uploadOrder Int duration Int @@ -31,6 +42,7 @@ model Video { uploadBatch UploadBatch? @relation(fields: [uploadBatchId], references: [id], onDelete: SetNull) transcription Transcription? webhooks Webhook[] + company Company @relation(fields: [companyId], references: [id]) } enum WebhookType { @@ -81,15 +93,19 @@ model TranscriptionSegment { model UploadBatch { id String @id @default(uuid()) + companyId String createdAt DateTime @default(now()) - videos Video[] + videos Video[] + company Company @relation(fields: [companyId], references: [id]) } model Tag { id String @id @default(uuid()) + companyId String slug String @unique createdAt DateTime @default(now()) - videos Video[] + videos Video[] + company Company @relation(fields: [companyId], references: [id]) } From b9f93e3658418ac24d201c099648e47692cf3ea7 Mon Sep 17 00:00:00 2001 From: Diego Fernandes Date: Thu, 1 Feb 2024 16:52:29 -0300 Subject: [PATCH 02/21] chore: create prisma adapter --- bun.lockb | Bin 409972 -> 410571 bytes package.json | 1 + .../migration.sql | 70 ++++++++ .../migration.sql | 25 +++ .../migration.sql | 9 ++ prisma/schema.prisma | 50 ++++++ src/@types/node-auth-adapters.d.ts | 7 + src/app/api/auth/[...nextauth]/route.ts | 1 - src/auth/auth.config.ts | 57 ++----- src/auth/credentials-provider.ts | 33 ++++ src/auth/google-provider.ts | 15 ++ src/auth/prisma-auth-adapter.ts | 153 ++++++++++++++++++ 12 files changed, 372 insertions(+), 49 deletions(-) create mode 100644 prisma/migrations/20240201190211_users_and_sessions_structure/migration.sql create mode 100644 prisma/migrations/20240201191323_alter_email_to_not_null/migration.sql create mode 100644 prisma/migrations/20240201192212_adjust_field_casing/migration.sql create mode 100644 src/@types/node-auth-adapters.d.ts create mode 100644 src/auth/credentials-provider.ts create mode 100644 src/auth/google-provider.ts create mode 100644 src/auth/prisma-auth-adapter.ts diff --git a/bun.lockb b/bun.lockb index b1f60a33463531cf0cabadc6b25c8a002edd504e..54a2dfc4789bb28874129b334cc99109ec144f28 100755 GIT binary patch delta 58537 zcmeFad2|&;yY{{JP6BBl%m|SgWt1Ru0wIK10tQ6}83YU=K!5~HK!`rDgMtz?BDPwf z49!LPB?&yzQ3!xHs(Cb_k7Pf?^)~nht_LfbzgPYTwUG0 zJG=PlhWOWSj=#N4%beoP`~VKff0xAr-#FhxinB6*!%e zRQOh_AH}YUeF#>*Z&&jC!-WqMP;KX;iu17fzpyf?Yr|2nYTcQ5@vqp*pdY2k-)8iG z%_ow69;K^7k$fJfVpYkpa5c?S#&^0$g0;iVs{8dn4Xc&fj&&tmX1m+VVd&czqnP`Cz>i7vRMOTKo8M)K5CjVGIFMApc-MT z!V|3Diu3(+!?BfLA8M`iGc@WPZLwL!5@I6=sMhc#iJu_PwXXht)ljoHNzJD#7eIrq7!2F|i3e!WJyN1|$ne~j@n z{Ugd(BmBjMzTK{=pLt$Z=H!gLnfdSHr`qLZWn|7I(`%afg*wfBeJ-}*tHA0;&o3c6 zKZvc~n9w0%W;RW;Tp>UpBQGzbV1he8x?wo4mG8Ep#aZ{v>C>j?U5T!2@y^mYdOz`M zqig*ZSbJ(~Kiw>AUkz*8^+@pjudx2TZTv}^r)quSbGY!1Huii((COCc;y~a$_-$D8 z_*Gc5pq1rm(3QxVCklvHVD9Cb(fxlBjbK#YQG z!8Jzt)$VKU5^HzBW(g`hL|Qdt_-KE;>bV=8+QCw{)Ooa8d7EY!r@y-5q&vSt?fAnX zzXMuN3TmtV`;sb1go1bht-|QuHh5KZ}qP3@`??E^*3$dGgxuc zp-SHcy99|-i$9~~EE$Efv&PTJ$ebF;UE%lW+pywySvw2X_$+fzS8g8s+HG8=c6i-t z-#6htzuYFUW?yBvCY+t`or{6&+{sya*)#J)=&EOAt(!SLAunrE)|}fO@XN}}ni3Ip+xtyi8n$yvBwVwd1xY|$n)t(5;o|>IIAz}K&z)#pJZau8#Rkrphn{VQizJDcH zerNmTIfM!bQ8uErb*yFWuh#nm(Ez(Levu3!=^_<~Tx?FR^G}P78~t=^U=^4|gH+** zVcCAV@WjA|r~C>uCPD?Ay5CR8d`Os(J#iwdkax9e@U-8ud|2m4|$^YXvzp(|d>O$RS=T8d+u62)8ukAd$!_Vt4H&`RFOHVBIbY=$sP0k7wv`fm> z4DP+l&to^N8dZDA=luM`9Ynwtd-8)qz}eZ_otR zM7_)1SEFIi@T>mWlsz#kFONKPu@zg}mN|2B-t<|MCbyq6Gb@+1GH|fe&+V4i{MJ9} zwyRnDidCp;!HiisGgPxzu(bqd7GzG#&Yu~Wm^VGAJ^IsDUkd9|chMfdu8~{ef{baN zf2CdSJvAFR9q3|J<{fuu&F10bd;KB@?(^qOcUYMmht)lwz^X(i%a6S6ucUSN`|Itb z=`+-VKzVE}1T$vk7GzGJkoD?Yetyrw)zIHQ;Fl9w0Ut*v|0#t}zvDaJ2P=bnY=T?h zD#|c#I=6TEfqz+hI;;wggVpm{*JkC;p$@I-)t`5SbpI{KlhCM+;(zIKkPTJD_n!p#^-v; z1Iw(xwZnlQDL~aZ@R6TjeBSiD3?{)oTW}U*HDhLW<`*CPr`3nB`k(vr$;$FY{HkDY zgw@OuNBsOIWaWD$wx;>&#`9rKmhrP#L-M%m&C1G3>=f=8c=A&}!-_a4qZt`fr)vOt zcFM||83@dpnavp$2)zB7-_Mz!`#o?4tO6S|eN>@Z_Dr3UKRve-{xiPt{l;fc@+uNb zyvFJKd;N*>Ib11>5c<;Zy3 zabNlQrDN+7b_cc!ZiRgTToG;v42@|JfOlpF617gd=b>So2 zPpQ|=ob<~$0jtadu=?RySpMoPjn9nZk(Slwgr5H}Wmxm-H2$^VuV7V>tD1MScL-YrMQ#q? z!q%uf0DBdNRiQbs(&Z{%XV?hq&=Xb#+FOUKYWfvxgsraqE;i^5$$PLWlAo1xt=5Xj zcLkH^YW3tO%l%=c>k6xZ307ZF+aH2saEiugPmJ$S0SDP9>-Y%=$NM$k3M-@G=ld<# z469{Ny4C8{4)?@Tf3$*C{didY@FBU#o-jRgW?pt~){m5;YJUJr&t-AN?=XNYKpFAj3S+8(1VRUdrD#J#{P&mf^`bstC- zVtO_pp{8^edVRPHc4OFF;HUcx_NF!qh@rxDotOl;7A_VCmzjhw7Ny%AW+8u+k5JrO&v??-ahf$(WWg zAuAJGV|p2^Mvo*tv)g^EezowwKg@R}PW5JJy!w9G#?NcU#eN=JU=3q_L2l;M?3sZ} z+WNyd5?upw7+Yt~%cRqQJ_~E29%|>;EwYf5VpGm2zO>*OXU2@czdMYdl|5}{c5eQC zwgQVSPk|eeq0`B4%nu#?PKm5Yo6yzR1Dm!qxU6z)y<6|j>ofMdftz9zdQ=V8b<7WGmNrF{P4 zI=xe!BnFLsFN}4U_DXdAuvU4uT3TYTr(250&9tZ+9x?8|-ig6oZd#wzn5N9*;ci@7 zvU5G5?p}IQI9se0?GEmd=tR@`z5J4UCpwp7DW9Mj>;#iyvHWuPz2zB-D(~*=n-~gJ zbzke<-RaH{^+1d{T*{Pp+4@O-pRp- z+@kco z_^ieHd$hjB>PuV|uQvy=?3~TpS&Y?|xR5u>&tYBe=h8DVrb0bN!K-P1LVdh)ZY8AD z)FPY|i_z6fT#YGGk#$TxKtD4~X;^Y(9#A0{OQY>gJm(WErSqnyb3T`BKNkj@Q^*;T z>fDaj8INdh$oFC?FQ)x-NwM6JX>OGB`eA_U3{7<&LQ~OnclX4YgYs~T`y~g@b4!P& z1}D4DuvF(>&UR&q4-;;mTZGnxC0Fw@s7dHtgXItS;68~?305j`^vq!OchT@v=fWnD zzT3x?xY~8DOm!YX)AXWp4E#sdigNd*COYSF6RBns$2^{jrRjx5Y0qFO9o;oN(K&(D z1}nzvyt)?#0zExTC&yK;Gcwh=AI;Be@Fj`ClWx(-)LZAkYPGrXF40xmh5P zj8)w$h^2;6ojpmh z)*ydMi@oS<{*=}OOZod{Ux#J0#B&3dM$4ak-(jVA7Ar=uwOcwa)hTEl(U_hw&tj#z zds32}p9u~0Q{Isjn{YN82IV#^n@vfXDEi;2z+kD;ECf9ggM-}CjMU%*t}{N>`HC3L zDSzJNyG3ZnFJ_JP%V6YFIpH*eX~(EUXRfvUHF>LBl$q-MfYu9-XtS>>3sJksIjR#R z(@mR@>TE~TIbGS+Tuop>ls}7UYGQD@>tr!s&{PVutzV+^v9*}#%-kMsDIW8f80t?> zQEIgrOa1{@?Tg}cq6qTWIA7f<5E$ZFgI`OE#Te#C%}k1A2(CQqHxA49Q>v%12K#ZF zlVTIgI*!7!dEmGnOG8lE3~o(xu`WBCWg2r&=RPM!T2d^AUq@y~@LRViJ2lwUE#-59 z>r6?Fd7RUrqq`?H+4-7~J+tYvi`>#Fsm??iOWpl81@GOIHZ`h`Lt%vhj(9hia*-*l z8gyds2?Qpf2GtRv+V{GP*LM%jbxYT$Iv=j^D?>joPIT(5j9hJ0o3U=uhE(TqG_BHH zoxVwOosGJRr)^Ai7OV;cm;{;?3%O2V>6~Kr%uRHD$LjA}tRTHt``0&rO`3%zPttML zKkt@4l^T&+X!jlX0SOK;_iWZae1G+dk^RT*mmZtghSn4-CIZ0}-iJWLl zIY};Yo##@W8_{%<;PFXPEQYf3yE(Wva@MH|y|L6Ds&HRou*gl@ni_oFE!vvuoFRrq zhExnksO4jBh3C6FcRqG@DlJcnwFa#pn&?z|JaRSLHzX+%u3_)`VtUgJt7r{tH-~h+1ukJAFsS3WO<8xg&W|L;VkpXAUQ4j-=reS^ zp7gV%aAs5?mft6;%1c=O{8DfKfu(fxHdfMl-;?RfsU;)jd=R^LWKT4pSEH76JCX$k(XBmjYZ;9 z?tmWMV`e?YCDTpcnCv`9NLiNCG>rKktD865TW>mBDQ3+KEM>`1Gh7?6)PAoT&M_?I z;GX~qPoJIS^u}VW;a)mTm`|`edY;Oq+cRf9iCbzd#)7_i537$`+&9^&`>fvqepwT+ z{OL%3F>A1TxqB8TJ6{p<`q@;|=~EKv4t2l`ET!?s;AyOZzBMq>`3plum8 zgINBoSW13Ju-X$x-)>BFYHan-a=*3%vD9PSH;hgUE_IzZxU{0#)cq1e&7XG{ztP

=R`X;VmRvY%8HC@h#RcW1q}Xkd$sxxZtwsB%Bs#mWE+!c_ns+5S7r$_}Y*vv> zEZb^E>t3wOy*g`!`5No0v&CJq-5*!~Hm?|~8*zSnO0m=)EGl*e%O6dwm;pQJbMNN6 z@N8%&A^(QF!p=yjKcOUV+7}YiC~>Y)r)RO$k2IQv_?WzWi>ITT zXNKFby84x2n(W3(#H!@2<5%p8cxrXN0ZS_#chVHJAFI2U^-|*Mz2sZ|=|2jqEpgtx zy0aLo7naw(yl5^xkm>|p_C3n$J|-qvmb+(Ea?Dgh9lcX;H6hiD4rEb2mowwVq zaIkyKfZgiYJwua&H@HOyQ=Rv){fW7BSfb;UMpk2<4q}F2c?08oM#%4Ibf@lXej5Kq zHyz8bneN#PmKG7}$vx_OZqfUxPWiuoMSq@K?Y_|^%%5pop0N_JSdO1hbbiM2drakz+~?l`bMmlst-|u#q^sOd zSauGOXZyD!gRR*z?X0B{+JZHR63B(_>##o%7-8b#={`bjeVsI0u`c(m`;uZ0_!aga zHhW|BF-y0EI^5ASmdB{H&r+R_(EN&Oax{1c4>z4}9LBrO=c&$%vLji?lzW$~y^$PF z$nPtiNcUm&!^^)NKZ>RP#mY}~8oU>YQ-da8sp-TqNRMFseewGiOEa5sA+E{6vnBIb zIngaWn(EX!6j_nfsY9_;H?A+#VS|n1(PSJCWA8`u(uvsDb&jPv*P&^;_+Q3svzB-L zaDK&73!=PfmhwSlNCt0BbZ$CpsoP(|;$&Zt?3_BQt2UiKjMP`Px$dk*8fQ0_zmh60 z`mjGynYOIJBeB%~)QZWl9Lo+dz4awl2d{9R ztnS#2rLH1#DqQ(UWNjfyXyOrlZR9+LsF5U}OOj$S{Q0QMR+~@Hp4<%Ub#Cc*sZI%+ z&7T{V6IdE1e_d(*+1crXe~w%FeX6qx&Ci~dC-||O_Cu;u^>cq>v3jxMU}n@_E&z7`eT%i)%R@NYAk;;YHEIf zHSTO&*RTBtUq8>KSSe@Yj$!>>`rY3|ZpKuXBUmHPdJa3`KNS19JcN~fHZJa4UcsKV zaFMpl1 z|H4uq`lW5QmiG`I{K<9xOm({d>W_eTZxJ&SiwC1o+_V$2^M&i#Z&-Qc13K3FVJ%!}5i{apN|a-p^S8|{~|dY#R)>Tmw+@|U>*SpJ;W9Gs^( z)hf8z6a|B+!PBM`aytI**Mr4`Gj+(88NWhwJk1)1rB3FAVKur3OBs+IYvDnx&b~E|W#>=d;&y{{ zeXPZj!6|edmKy1JUwo8c`9r&JeWDZdm){*!nCF<0SQ-qzRMOgubvc&TjWNfu{Cm~L zfuMI1axx7{bcSHH#>-!?!)N2PdT+WQ}o1$2H`V69` zcE}8_5=;t(f^Nk>Qk)w@L9bdA!Iv4kOp$|YC0>MGfh)COf)`-vWc0uC`id8Aqx?9& zQk!RrDv;ngUP_Mk#H9zhSn`&502X-`CBrA%0E?n1m49qL)SksFTXPajUJ;4 zBCn@-g*(u9;asa!F=+3pnFBFbU~%u6mKVs&eJ3!N65sxl;(SkW+&f301AniD zn!D4mZ0}R+9azJ?INimZ$BS&G^A;9o3YKOU7B_=WVyQ(8C{O>tVU6`l9#M-Hd0y&; zZCE-N%DMaQO){mm=%zO3ogLtuW5#&@#3c)Q|L^&gkp0tS9TmJ}gz6#jkr}aEM8($8=xE>-CXt+yJiR4e1HL_9>ik zShhE~t67GnLbyNS4BBhsxPGK0hH5r4uhkEB4}M^r24vW&vCax!x&^13A_;2|c%#J^ zmIPD)s~#7jQ6lJQ#1-UmeMGYC=*HOoNmGHi7zaSWxg2fujm%tBV`4=Cae4LY5 zerKx}k}nK;m*N;xtwAu+S%7J$IN#+yXVMx|k+Hm-A8XPZ2a}x-2qtK}ix{}D?$_-R_V;JnA; zN=TY1SVO%^#JoVTpC62FsiF3&Hj1FXSgDl{V+|oOE9K0jU@J4AX|TI9m%kd&iN&>v zLEeR>{JHF~zz6w@fu6EVwMumBO(v}w-TFDIU%#b25<~S`o7b8#V&e?0OLaE34tn?L zvF^Ux5}nVn71(WR{N9hBHBcTmNo{@ z7^fxIWX3Pu zmlTVkcJo|wvuEgXSIx_#Wk>(w7i*R}L2f)S{lSW<&z~+_?tP1rL&zUYX9qzQLQczg zYZLU|5%L1|U}7-Yl(q>bhq-CgZ1oOt2r%Ycf4$phK7E$X8Kb|K(R-3Wc`y~|D9cgU zxj^ZsTb=>y_y-OLVu+XpRKRSYqbw_5F7= z{LNwCu?SHK7AuxRtOUhC`fWgmSo%^K9RI|snCr#*EdRTK@-bEy%U(wL-m&Z-SPAb1 z%5bHP|0kB;eKx)~-05NQ&MHG=p{k9Xo*Zoj|$m zvi3``4zcuCfc#zsI{rJ3CH^hX%VXWC1_FD5c%O|BE93pv{wJ3I0U&Uu-wX7yS%j>i5y}DEAkQd1T(EY8|ME&fj%rNgX^%BZXR3#US#cCVZ|52 zYQQR3GvZM=4t^Te0KTZ7JF38!;Y#p6Yaf7Bf%oC_;h$i&=yzCCE{a4-SB?)Aq@P+U z!&cLtB+rrAHGc3PESbq9Br^*{Pm_Tg;li?ch^{{HV2v&`k!n%O1fR({Y%d23u z`~hn}2nV-AZMRARl%Q8&}JZ6(UZdpHK)ge~bqd2f7$Y@u&TA& za;eq%Ij?{3xfg-|1N*JR0qdaOPb;H?u+GWD*4FQY)qzK?eGKORz}ME+?}?S}J6P>M zY3-ja|7!K$qnKK9JcFPXMB%NLRfLtW3LiuTs>`q}yB4-ubRMh$XlU(5mK(!e(MMbR zDp-eD>Bm_fAGQveR+tE@WjU}~oM(Bq<$u9C#Hzqt%kyAWk=HCXxX zfoW(saDV{+2M*eZ4=jIV^-p0H^p(|5Sp5fBJ#`A!+>au66%flu3%D)pom8+2>;fyF zp0FC&%VWmBw-x%p%5Wg8go9yaQ~)=HSHb)rc#sbj_#{l(fu~@l+XAbAov;dc5!Ok2 z2v*O20P85r(OOzRwh2Cgb-I6J6MSnEh?U`Y)-KCxnRX>m1x{PP->lz%!eK2V3 zQV~``+EhV@ST(O~xvGt?X1O}7qb$q6ru9G1`qj2v2UZU?f_1?D`QOwE{}Wb)o7wbY z+09{9M8CwA-O9#`)l==P-L9NH|Mk0A9bzTuXl=22CJ9!CU14>33M{|wu#SIXosO5- zbZIu-r8ZrBV(%gft3tQHN`I@37poz+!>Z^VR{tj^T{z%c2eA^| zWo@xCzT4Vj#V@n^a;ukRRqR0<|B&T}t)EyGUIQzi$2@&pB;d0Oe4GgJ6P|<5O1R$Y z8(_8YX=^`Y{r-_naZm2wlzfYIf6nq&SoPRulZ#dC3zoNAT`a$y*8V5f?b2QwFIGPL zEWceYU~2RVR?|uo*l)!HF#iYMxAq4zIK)c)A*@0_vbtFIC)O^@l8*ABbYI$dvFu~k zhQnT`ePac&fM2_Kd}<-wT@yX+-GgE3OE4E?_H~xWp(|BRu?P% zVQY)A!-0>i@K3A)ju5YmzO;T~WpoTy{MS|&OaBJe#pWkiJ#q@x961B)5K9l@C~U6GpxW68!cA!P({O6T3sxC1gu7mvbtFM zXv772%Ke|7*nk+Rz^9nmWVm93M_#2JiQo}zY8nFyI~z- z*~=}jfSY6Qf;H;9VdYaAOZf`ChM=P?YdXAX6YRGM4p@H2^1HAqc+lF1U>#yr;6tk) zhE?#Vu<|`>K4TZVWp3PmA*Wz z_zE^YT+s@ZVVw)LU?r#x>kuo!`IhV3_=eVQV!0`-0$N&5fK`Duuv*>$);A74t=$ji z1PTW(C7@@L5wJ423RVTi!aDwmmGL;@#aCO-usq&!Cai)d!^(FWtT{8+#?OOQe!vg- ztPI0m#DC@L=u6R+(OovaEK6UGuJrdcIwUbePaey>=&EGwVgRu}7wr@(7g5G unKSb9i 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 delta 57643 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


|3nSYbJaaN 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 { + const { name, email, image } = userToCreate + const [, domain] = email.split('@') + + const company = await prisma.company.findUniqueOrThrow({ + where: { + domain, + }, + }) + + return await prisma.user.create({ + data: { + name, + email, + image, + companyId: company.id, + emailVerified: new Date(), + }, + }) + }, + + async getUser(id) { + const user = await prisma.user.findUnique({ + where: { id }, + }) + + if (!user) { + return null + } + + return user + }, + + async getUserByEmail(email) { + const user = await prisma.user.findUnique({ + where: { email }, + }) + + if (!user) { + return null + } + + return user + }, + + async getUserByAccount({ providerAccountId, provider }) { + const account = await prisma.account.findUnique({ + where: { + provider_providerAccountId: { + provider, + providerAccountId, + }, + }, + include: { + user: true, + }, + }) + + if (!account) { + return null + } + + const { user } = account + + return user + }, + + async updateUser(userToUpdate) { + const { id, name, email, image } = userToUpdate + + const user = await prisma.user.update({ + where: { id }, + data: { + name, + email, + image, + }, + }) + + return user + }, + + async linkAccount(account) { + await prisma.account.create({ + data: { + userId: account.userId, + type: account.type, + provider: account.provider, + providerAccountId: account.providerAccountId, + refreshToken: account.refresh_token, + accessToken: account.access_token, + expiresAt: account.expires_at, + tokenType: account.token_type, + scope: account.scope, + idToken: account.id_token, + sessionState: account.session_state?.toString(), + }, + }) + }, + + async createSession({ sessionToken, userId, expires }) { + const session = await prisma.session.create({ + data: { + userId, + expires, + sessionToken, + }, + }) + + return session + }, + + async getSessionAndUser(sessionToken) { + const prismaSession = await prisma.session.findUnique({ + where: { sessionToken }, + include: { + user: true, + }, + }) + + if (!prismaSession) { + return null + } + + const { user, ...session } = prismaSession + + return { user, session } + }, + + async updateSession({ sessionToken, userId, expires }) { + const session = await prisma.session.update({ + where: { sessionToken }, + data: { + expires, + userId, + }, + }) + + return session + }, + + async deleteSession(sessionToken) { + await prisma.session.delete({ + where: { sessionToken }, + }) + }, +} From e8db9d26aff9fe93a3fd03651998490ddfcb594f Mon Sep 17 00:00:00 2001 From: Diego Fernandes Date: Fri, 2 Feb 2024 17:09:59 -0300 Subject: [PATCH 03/21] chore: migrate from prisma to drizzle on routes --- .vscode/settings.json | 1 + bun.lockb | Bin 410571 -> 461818 bytes docker-compose.yml | 16 - drizzle.config.ts | 12 + drizzle/0000_cute_obadiah_stane.sql | 203 +++++ drizzle/0001_massive_jack_murdock.sql | 1 + drizzle/meta/0000_snapshot.json | 857 ++++++++++++++++++ drizzle/meta/0001_snapshot.json | 840 +++++++++++++++++ drizzle/meta/_journal.json | 20 + package.json | 6 + .../migration.sql | 53 -- .../migration.sql | 8 - .../migration.sql | 11 - .../migration.sql | 8 - .../migration.sql | 3 - .../migration.sql | 23 - .../migration.sql | 16 - .../migration.sql | 10 - .../migration.sql | 8 - .../migration.sql | 8 - .../migration.sql | 2 - .../migration.sql | 8 - .../20230713204140_test_column/migration.sql | 2 - .../migration.sql | 30 - .../migration.sql | 2 - .../migration.sql | 2 - .../migration.sql | 2 - .../migration.sql | 2 - .../migration.sql | 2 - .../migration.sql | 26 - .../migration.sql | 3 - .../migration.sql | 2 - .../migration.sql | 14 - .../migration.sql | 25 - .../migration.sql | 34 - .../migration.sql | 70 -- .../migration.sql | 25 - .../migration.sql | 9 - prisma/migrations/migration_lock.toml | 3 - prisma/schema.prisma | 161 ---- server/app.ts | 11 +- server/routes/create-tag.ts | 12 +- server/routes/create-upload-batch.ts | 84 +- server/routes/delete-upload.ts | 36 +- server/routes/download-upload-media.ts | 14 +- server/routes/generate-ai-description.ts | 27 +- server/routes/get-tags.ts | 35 +- server/routes/get-upload-batch.ts | 22 +- server/routes/get-upload-transcription.ts | 14 +- server/routes/get-upload-webhooks.ts | 12 +- server/routes/get-upload.ts | 47 + server/routes/get-uploads.ts | 93 +- server/routes/update-upload-transcription.ts | 36 - server/routes/update-upload.ts | 87 +- src/app/(app)/videos/[id]/page.tsx | 14 +- .../(app)/videos/[id]/tabs/overview/index.tsx | 26 +- src/auth/auth.config.ts | 2 - src/auth/prisma-auth-adapter.ts | 153 ---- src/components/summary/storage.tsx | 40 +- src/components/summary/total-count.tsx | 32 +- src/drizzle/client.ts | 9 + src/drizzle/migrate.ts | 14 + src/drizzle/schema/account.ts | 38 + src/drizzle/schema/company.ts | 24 + src/drizzle/schema/index.ts | 12 + src/drizzle/schema/session.ts | 36 + src/drizzle/schema/tag-to-video.ts | 33 + src/drizzle/schema/tag.ts | 40 + src/drizzle/schema/transcription-segment.ts | 27 + src/drizzle/schema/transcription.ts | 34 + src/drizzle/schema/upload-batch.ts | 25 + src/drizzle/schema/user.ts | 41 + src/drizzle/schema/verification-token.ts | 18 + src/drizzle/schema/video.ts | 67 ++ src/drizzle/schema/webhook.ts | 39 + src/env.ts | 29 +- src/middleware.ts | 3 +- tsconfig.json | 2 +- 78 files changed, 2786 insertions(+), 1030 deletions(-) delete mode 100644 docker-compose.yml create mode 100644 drizzle.config.ts create mode 100644 drizzle/0000_cute_obadiah_stane.sql create mode 100644 drizzle/0001_massive_jack_murdock.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/0001_snapshot.json create mode 100644 drizzle/meta/_journal.json delete mode 100644 prisma/migrations/20230710193825_create_base_tables/migration.sql delete mode 100644 prisma/migrations/20230710220026_alter_duration_to_not_nullable/migration.sql delete mode 100644 prisma/migrations/20230710221828_add_external_provider_id_to_videos/migration.sql delete mode 100644 prisma/migrations/20230711181144_add_audio_storage_key/migration.sql delete mode 100644 prisma/migrations/20230711183118_add_transcription_and_processed_at_fields_to_video/migration.sql delete mode 100644 prisma/migrations/20230711194053_create_transcription/migration.sql delete mode 100644 prisma/migrations/20230712011227_create_transcription_segments/migration.sql delete mode 100644 prisma/migrations/20230712011745_change_decimal_range/migration.sql delete mode 100644 prisma/migrations/20230713020533_add_size_in_bytes_to_videos/migration.sql delete mode 100644 prisma/migrations/20230713024123_add_upload_order/migration.sql delete mode 100644 prisma/migrations/20230713151613_add_reviewed_at_to_transcriptions/migration.sql delete mode 100644 prisma/migrations/20230713151720_add_reviewed_at_to_transcriptions/migration.sql delete mode 100644 prisma/migrations/20230713204140_test_column/migration.sql delete mode 100644 prisma/migrations/20230715225245_create_webhooks/migration.sql delete mode 100644 prisma/migrations/20230715225353_alter_webhook_status_default_value/migration.sql delete mode 100644 prisma/migrations/20230715230658_add_update_external_provider_status_webhook_type/migration.sql delete mode 100644 prisma/migrations/20230719205748_add_commit_url_to_videos/migration.sql delete mode 100644 prisma/migrations/20230724165750_create_new_webhook_type/migration.sql delete mode 100644 prisma/migrations/20230724165941_add_subtitles_storage_key_to_videos/migration.sql delete mode 100644 prisma/migrations/20230726142319_add_cascade_to_relations/migration.sql delete mode 100644 prisma/migrations/20230730162726_allow_null_storage_or_audio_storage_keys/migration.sql delete mode 100644 prisma/migrations/20240123190935_add_language_to_videos/migration.sql delete mode 100644 prisma/migrations/20240201182601_create_companies/migration.sql delete mode 100644 prisma/migrations/20240201182915_add_company_id_reference_to_tables/migration.sql delete mode 100644 prisma/migrations/20240201183144_set_company_id_as_not_null/migration.sql delete mode 100644 prisma/migrations/20240201190211_users_and_sessions_structure/migration.sql delete mode 100644 prisma/migrations/20240201191323_alter_email_to_not_null/migration.sql delete mode 100644 prisma/migrations/20240201192212_adjust_field_casing/migration.sql delete mode 100644 prisma/migrations/migration_lock.toml delete mode 100644 prisma/schema.prisma create mode 100644 server/routes/get-upload.ts delete mode 100644 server/routes/update-upload-transcription.ts delete mode 100644 src/auth/prisma-auth-adapter.ts create mode 100644 src/drizzle/client.ts create mode 100644 src/drizzle/migrate.ts create mode 100644 src/drizzle/schema/account.ts create mode 100644 src/drizzle/schema/company.ts create mode 100644 src/drizzle/schema/index.ts create mode 100644 src/drizzle/schema/session.ts create mode 100644 src/drizzle/schema/tag-to-video.ts create mode 100644 src/drizzle/schema/tag.ts create mode 100644 src/drizzle/schema/transcription-segment.ts create mode 100644 src/drizzle/schema/transcription.ts create mode 100644 src/drizzle/schema/upload-batch.ts create mode 100644 src/drizzle/schema/user.ts create mode 100644 src/drizzle/schema/verification-token.ts create mode 100644 src/drizzle/schema/video.ts create mode 100644 src/drizzle/schema/webhook.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 1a73a5e..13824f2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,7 @@ "hookform", "immer", "Immer", + "neondatabase", "NEXTAUTH", "openai", "PANDAVIDEO", diff --git a/bun.lockb b/bun.lockb index 54a2dfc4789bb28874129b334cc99109ec144f28..c86343496745af133ab82e61a0e6e7374c014126 100755 GIT binary patch delta 114538 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 O$0GUH>)HQS{Qm(Y)hp%z delta 83138 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

lpN#z|72w!^@G~}f zhmIb9n5A}e4vryyfvi-`DwCF>w~vMdH#+lLJ$lr#@+l`s6g6r+%1>@C!_b@fE}Oql z>Vz#ap1YzNy)}y8f>RrE5FE;(QrCtcrv!#of7dDKuvM*SN6UL0n7c!s59C9Wm)cezp{r zh!tp>Rs3b<3dFePFUMDCzLu^5@^%Hb$%g~Pz7pY=0wi=LUXmxi4nngxhccPHQVY{t z+~bv60HgV=(rV})KdsWLv+rzQh4FV2%E0DRIZN!~uCI5la&t}K)) z>hY+3OJQRrap*ztsva@dS%*BG6{F+&5Q#*mO{z0n?#z(QmR$b$TPq>6K4^Y&$;FmRQohkr$>9%L zV>#q(OD`>6LnP=8j?SLSe=fh(TqW!yt}4IP>dBFhTF$9AZJJzqt8I*J`Vozhsklkn zH(D+Ud5@W<-y1EPT({UelBIi01`d##z=c&CL)y(KogJo}&}lV9Fzb!D#2mQwDx z*h)+1%C_YAXvS7YVw2lSN%JbUrm>SMPqf;{8g?E~#tT(nYgKK@t*w9R+Ll*Y#4XI0Og2=tb(D^;VHtZho2T@7t$Ar}LJIxR%`ofS z*U0JeYb~`bd!wzi^=kQj_qJVSaa!AYmM@j`sAemtn`!*tT5BorPTQd0)z)ljr0@sr zN6Bxsjgeh7ZMrdI;a|!ko2uDL%D7~<#`3C~t+w1t22;3Hv!#ozSKao|Vi+!Ga$7N} z6lSX`&63-i@*g+(_)1F_n=*y1rL)WoL&O2Nm#(h1>@u#7Etj-#Md9zKLSwj;%C=Tg z*22~MFk3U4E2T_LjkI(TzpA$Mu|L+bt#Ov8b!^^JJdMpq`lYd5mEm=4-C|q1*_v9! zuP%(UFTJgYgqv3xq;3XVEjd}&R#1**u-&C}L2*lqT>fxJ+SlA*_9I`}QpEmP&-Qm= z8Cl*IC^189$znSWu|3O;X$|#IHdad>``0?#B8%KO0vpZQ0$s*#vDNVxS#Db-B@Ss` gv9BN47HF}_KHAc_$j1surSlEV7VBY2b~nZU2Z;D$4gdfE diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index e7e3339..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,16 +0,0 @@ -version: '3.7' - -services: - postgres: - image: bitnami/postgresql:latest - ports: - - '5432:5432' - environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=docker - - POSTGRES_DB=jupiter - volumes: - - jupiter_pg_data:/bitnami/postgresql - -volumes: - jupiter_pg_data: \ No newline at end of file diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..96295a0 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,12 @@ +import type { Config } from 'drizzle-kit' + +import { env } from '@/env' + +export default { + schema: './src/drizzle/schema/index.ts', + out: './drizzle', + driver: 'pg', + dbCredentials: { + connectionString: env.DATABASE_URL, + }, +} satisfies Config diff --git a/drizzle/0000_cute_obadiah_stane.sql b/drizzle/0000_cute_obadiah_stane.sql new file mode 100644 index 0000000..2533bb6 --- /dev/null +++ b/drizzle/0000_cute_obadiah_stane.sql @@ -0,0 +1,203 @@ +DO $$ BEGIN + CREATE TYPE "WebhookStatus" AS ENUM('ERROR', 'SUCCESS', 'RUNNING'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + CREATE TYPE "WebhookType" AS ENUM('CREATE_SUBTITLES_FROM_TRANSCRIPTION', 'UPDATE_EXTERNAL_PROVIDER_STATUS', 'UPLOAD_TO_EXTERNAL_PROVIDER', 'CREATE_TRANSCRIPTION', 'PROCESS_VIDEO'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Account" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "userId" uuid NOT NULL, + "type" text NOT NULL, + "provider" text NOT NULL, + "providerAccountId" text NOT NULL, + "scope" text, + "accessToken" text, + "expiresAt" integer, + "idToken" text, + "refreshToken" text, + "sessionState" text, + "tokenType" text +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Company" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "domain" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Session" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "sessionToken" text NOT NULL, + "userId" uuid NOT NULL, + "expires" timestamp(3) NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Tag" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "slug" text NOT NULL, + "createdAt" timestamp(3) DEFAULT now() NOT NULL, + "companyId" uuid NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "_TagToVideo" ( + "A" uuid NOT NULL, + "B" uuid NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "test" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Transcription" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "videoId" uuid NOT NULL, + "createdAt" timestamp(3) DEFAULT now() NOT NULL, + "reviewedAt" timestamp(3) +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "TranscriptionSegment" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "transcriptionId" uuid NOT NULL, + "start" numeric(10, 2) NOT NULL, + "end" numeric(10, 2) NOT NULL, + "text" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "UploadBatch" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "createdAt" timestamp(3) DEFAULT now() NOT NULL, + "companyId" uuid NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "User" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "companyId" uuid NOT NULL, + "name" text, + "email" text NOT NULL, + "emailVerified" timestamp(3), + "image" text +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "VerificationToken" ( + "identifier" text NOT NULL, + "token" text NOT NULL, + "expires" timestamp(3) NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Video" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "duration" integer NOT NULL, + "title" text NOT NULL, + "storageKey" text, + "description" text, + "uploadBatchId" uuid, + "companyId" uuid NOT NULL, + "externalProviderId" text, + "audioStorageKey" text, + "processedAt" timestamp(3), + "sizeInBytes" integer NOT NULL, + "uploadOrder" integer NOT NULL, + "commitUrl" text, + "subtitlesStorageKey" text, + "language" text DEFAULT 'pt' NOT NULL, + "createdAt" timestamp(3) DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Webhook" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "videoId" uuid NOT NULL, + "type" "WebhookType" NOT NULL, + "status" "WebhookStatus" DEFAULT 'RUNNING' NOT NULL, + "createdAt" timestamp(3) DEFAULT now() NOT NULL, + "finishedAt" timestamp(3), + "metadata" text +); +--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "Account_provider_providerAccountId_key" ON "Account" ("provider","providerAccountId");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "Company_domain_key" ON "Company" ("domain");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "Session_sessionToken_key" ON "Session" ("sessionToken");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "Tag_slug_key" ON "Tag" ("slug");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "_TagToVideo_AB_unique" ON "_TagToVideo" ("A","B");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "_TagToVideo_B_index" ON "_TagToVideo" ("B");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "Transcription_videoId_key" ON "Transcription" ("videoId");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "User_email_key" ON "User" ("email");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "VerificationToken_token_key" ON "VerificationToken" ("token");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "VerificationToken_identifier_token_key" ON "VerificationToken" ("identifier","token");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "Video_externalProviderId_key" ON "Video" ("externalProviderId");--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Tag" ADD CONSTRAINT "Tag_companyId_Company_id_fk" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE restrict ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "_TagToVideo" ADD CONSTRAINT "_TagToVideo_A_Tag_id_fk" FOREIGN KEY ("A") REFERENCES "Tag"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "_TagToVideo" ADD CONSTRAINT "_TagToVideo_B_Video_id_fk" FOREIGN KEY ("B") REFERENCES "Video"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Transcription" ADD CONSTRAINT "Transcription_videoId_Video_id_fk" FOREIGN KEY ("videoId") REFERENCES "Video"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "TranscriptionSegment" ADD CONSTRAINT "TranscriptionSegment_transcriptionId_Transcription_id_fk" FOREIGN KEY ("transcriptionId") REFERENCES "Transcription"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "UploadBatch" ADD CONSTRAINT "UploadBatch_companyId_Company_id_fk" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE restrict ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "User" ADD CONSTRAINT "User_companyId_Company_id_fk" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE restrict ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Video" ADD CONSTRAINT "Video_uploadBatchId_UploadBatch_id_fk" FOREIGN KEY ("uploadBatchId") REFERENCES "UploadBatch"("id") ON DELETE set null ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Video" ADD CONSTRAINT "Video_companyId_Company_id_fk" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE restrict ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Webhook" ADD CONSTRAINT "Webhook_videoId_Video_id_fk" FOREIGN KEY ("videoId") REFERENCES "Video"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/drizzle/0001_massive_jack_murdock.sql b/drizzle/0001_massive_jack_murdock.sql new file mode 100644 index 0000000..1ace09a --- /dev/null +++ b/drizzle/0001_massive_jack_murdock.sql @@ -0,0 +1 @@ +DROP TABLE "test"; \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..1a5df77 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,857 @@ +{ + "id": "c9ca6640-787b-4b51-80b4-a18237571824", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "5", + "dialect": "pg", + "tables": { + "Account": { + "name": "Account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "idToken": { + "name": "idToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sessionState": { + "name": "sessionState", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokenType": { + "name": "tokenType", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Account_provider_providerAccountId_key": { + "name": "Account_provider_providerAccountId_key", + "columns": [ + "provider", + "providerAccountId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Account_userId_User_id_fk": { + "name": "Account_userId_User_id_fk", + "tableFrom": "Account", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Company": { + "name": "Company", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "Company_domain_key": { + "name": "Company_domain_key", + "columns": [ + "domain" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Session": { + "name": "Session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "Session_sessionToken_key": { + "name": "Session_sessionToken_key", + "columns": [ + "sessionToken" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Session_userId_User_id_fk": { + "name": "Session_userId_User_id_fk", + "tableFrom": "Session", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Tag": { + "name": "Tag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "companyId": { + "name": "companyId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "Tag_slug_key": { + "name": "Tag_slug_key", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Tag_companyId_Company_id_fk": { + "name": "Tag_companyId_Company_id_fk", + "tableFrom": "Tag", + "tableTo": "Company", + "columnsFrom": [ + "companyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "_TagToVideo": { + "name": "_TagToVideo", + "schema": "", + "columns": { + "A": { + "name": "A", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "B": { + "name": "B", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "_TagToVideo_AB_unique": { + "name": "_TagToVideo_AB_unique", + "columns": [ + "A", + "B" + ], + "isUnique": true + }, + "_TagToVideo_B_index": { + "name": "_TagToVideo_B_index", + "columns": [ + "B" + ], + "isUnique": false + } + }, + "foreignKeys": { + "_TagToVideo_A_Tag_id_fk": { + "name": "_TagToVideo_A_Tag_id_fk", + "tableFrom": "_TagToVideo", + "tableTo": "Tag", + "columnsFrom": [ + "A" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "_TagToVideo_B_Video_id_fk": { + "name": "_TagToVideo_B_Video_id_fk", + "tableFrom": "_TagToVideo", + "tableTo": "Video", + "columnsFrom": [ + "B" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "test": { + "name": "test", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Transcription": { + "name": "Transcription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "videoId": { + "name": "videoId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "reviewedAt": { + "name": "reviewedAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Transcription_videoId_key": { + "name": "Transcription_videoId_key", + "columns": [ + "videoId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Transcription_videoId_Video_id_fk": { + "name": "Transcription_videoId_Video_id_fk", + "tableFrom": "Transcription", + "tableTo": "Video", + "columnsFrom": [ + "videoId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "TranscriptionSegment": { + "name": "TranscriptionSegment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "transcriptionId": { + "name": "transcriptionId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "start": { + "name": "start", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "end": { + "name": "end", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "TranscriptionSegment_transcriptionId_Transcription_id_fk": { + "name": "TranscriptionSegment_transcriptionId_Transcription_id_fk", + "tableFrom": "TranscriptionSegment", + "tableTo": "Transcription", + "columnsFrom": [ + "transcriptionId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "UploadBatch": { + "name": "UploadBatch", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "companyId": { + "name": "companyId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "UploadBatch_companyId_Company_id_fk": { + "name": "UploadBatch_companyId_Company_id_fk", + "tableFrom": "UploadBatch", + "tableTo": "Company", + "columnsFrom": [ + "companyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "User": { + "name": "User", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "companyId": { + "name": "companyId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "User_email_key": { + "name": "User_email_key", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": { + "User_companyId_Company_id_fk": { + "name": "User_companyId_Company_id_fk", + "tableFrom": "User", + "tableTo": "Company", + "columnsFrom": [ + "companyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "VerificationToken": { + "name": "VerificationToken", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "VerificationToken_token_key": { + "name": "VerificationToken_token_key", + "columns": [ + "token" + ], + "isUnique": true + }, + "VerificationToken_identifier_token_key": { + "name": "VerificationToken_identifier_token_key", + "columns": [ + "identifier", + "token" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Video": { + "name": "Video", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storageKey": { + "name": "storageKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploadBatchId": { + "name": "uploadBatchId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "companyId": { + "name": "companyId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "externalProviderId": { + "name": "externalProviderId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audioStorageKey": { + "name": "audioStorageKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processedAt": { + "name": "processedAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false + }, + "sizeInBytes": { + "name": "sizeInBytes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "uploadOrder": { + "name": "uploadOrder", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "commitUrl": { + "name": "commitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subtitlesStorageKey": { + "name": "subtitlesStorageKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pt'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "Video_externalProviderId_key": { + "name": "Video_externalProviderId_key", + "columns": [ + "externalProviderId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Video_uploadBatchId_UploadBatch_id_fk": { + "name": "Video_uploadBatchId_UploadBatch_id_fk", + "tableFrom": "Video", + "tableTo": "UploadBatch", + "columnsFrom": [ + "uploadBatchId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Video_companyId_Company_id_fk": { + "name": "Video_companyId_Company_id_fk", + "tableFrom": "Video", + "tableTo": "Company", + "columnsFrom": [ + "companyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Webhook": { + "name": "Webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "videoId": { + "name": "videoId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "WebhookType", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "WebhookStatus", + "primaryKey": false, + "notNull": true, + "default": "'RUNNING'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finishedAt": { + "name": "finishedAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Webhook_videoId_Video_id_fk": { + "name": "Webhook_videoId_Video_id_fk", + "tableFrom": "Webhook", + "tableTo": "Video", + "columnsFrom": [ + "videoId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": { + "WebhookStatus": { + "name": "WebhookStatus", + "values": { + "ERROR": "ERROR", + "SUCCESS": "SUCCESS", + "RUNNING": "RUNNING" + } + }, + "WebhookType": { + "name": "WebhookType", + "values": { + "CREATE_SUBTITLES_FROM_TRANSCRIPTION": "CREATE_SUBTITLES_FROM_TRANSCRIPTION", + "UPDATE_EXTERNAL_PROVIDER_STATUS": "UPDATE_EXTERNAL_PROVIDER_STATUS", + "UPLOAD_TO_EXTERNAL_PROVIDER": "UPLOAD_TO_EXTERNAL_PROVIDER", + "CREATE_TRANSCRIPTION": "CREATE_TRANSCRIPTION", + "PROCESS_VIDEO": "PROCESS_VIDEO" + } + } + }, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..505e457 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,840 @@ +{ + "id": "4f813968-c2e4-4c6e-a596-a5bcddf83665", + "prevId": "c9ca6640-787b-4b51-80b4-a18237571824", + "version": "5", + "dialect": "pg", + "tables": { + "Account": { + "name": "Account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "idToken": { + "name": "idToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sessionState": { + "name": "sessionState", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokenType": { + "name": "tokenType", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Account_provider_providerAccountId_key": { + "name": "Account_provider_providerAccountId_key", + "columns": [ + "provider", + "providerAccountId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Account_userId_User_id_fk": { + "name": "Account_userId_User_id_fk", + "tableFrom": "Account", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Company": { + "name": "Company", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "Company_domain_key": { + "name": "Company_domain_key", + "columns": [ + "domain" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Session": { + "name": "Session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "Session_sessionToken_key": { + "name": "Session_sessionToken_key", + "columns": [ + "sessionToken" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Session_userId_User_id_fk": { + "name": "Session_userId_User_id_fk", + "tableFrom": "Session", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Tag": { + "name": "Tag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "companyId": { + "name": "companyId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "Tag_slug_key": { + "name": "Tag_slug_key", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Tag_companyId_Company_id_fk": { + "name": "Tag_companyId_Company_id_fk", + "tableFrom": "Tag", + "tableTo": "Company", + "columnsFrom": [ + "companyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "_TagToVideo": { + "name": "_TagToVideo", + "schema": "", + "columns": { + "A": { + "name": "A", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "B": { + "name": "B", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "_TagToVideo_AB_unique": { + "name": "_TagToVideo_AB_unique", + "columns": [ + "A", + "B" + ], + "isUnique": true + }, + "_TagToVideo_B_index": { + "name": "_TagToVideo_B_index", + "columns": [ + "B" + ], + "isUnique": false + } + }, + "foreignKeys": { + "_TagToVideo_A_Tag_id_fk": { + "name": "_TagToVideo_A_Tag_id_fk", + "tableFrom": "_TagToVideo", + "tableTo": "Tag", + "columnsFrom": [ + "A" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "_TagToVideo_B_Video_id_fk": { + "name": "_TagToVideo_B_Video_id_fk", + "tableFrom": "_TagToVideo", + "tableTo": "Video", + "columnsFrom": [ + "B" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Transcription": { + "name": "Transcription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "videoId": { + "name": "videoId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "reviewedAt": { + "name": "reviewedAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Transcription_videoId_key": { + "name": "Transcription_videoId_key", + "columns": [ + "videoId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Transcription_videoId_Video_id_fk": { + "name": "Transcription_videoId_Video_id_fk", + "tableFrom": "Transcription", + "tableTo": "Video", + "columnsFrom": [ + "videoId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "TranscriptionSegment": { + "name": "TranscriptionSegment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "transcriptionId": { + "name": "transcriptionId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "start": { + "name": "start", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "end": { + "name": "end", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "TranscriptionSegment_transcriptionId_Transcription_id_fk": { + "name": "TranscriptionSegment_transcriptionId_Transcription_id_fk", + "tableFrom": "TranscriptionSegment", + "tableTo": "Transcription", + "columnsFrom": [ + "transcriptionId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "UploadBatch": { + "name": "UploadBatch", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "companyId": { + "name": "companyId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "UploadBatch_companyId_Company_id_fk": { + "name": "UploadBatch_companyId_Company_id_fk", + "tableFrom": "UploadBatch", + "tableTo": "Company", + "columnsFrom": [ + "companyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "User": { + "name": "User", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "companyId": { + "name": "companyId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "User_email_key": { + "name": "User_email_key", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": { + "User_companyId_Company_id_fk": { + "name": "User_companyId_Company_id_fk", + "tableFrom": "User", + "tableTo": "Company", + "columnsFrom": [ + "companyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "VerificationToken": { + "name": "VerificationToken", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "VerificationToken_token_key": { + "name": "VerificationToken_token_key", + "columns": [ + "token" + ], + "isUnique": true + }, + "VerificationToken_identifier_token_key": { + "name": "VerificationToken_identifier_token_key", + "columns": [ + "identifier", + "token" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Video": { + "name": "Video", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storageKey": { + "name": "storageKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploadBatchId": { + "name": "uploadBatchId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "companyId": { + "name": "companyId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "externalProviderId": { + "name": "externalProviderId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audioStorageKey": { + "name": "audioStorageKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processedAt": { + "name": "processedAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false + }, + "sizeInBytes": { + "name": "sizeInBytes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "uploadOrder": { + "name": "uploadOrder", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "commitUrl": { + "name": "commitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subtitlesStorageKey": { + "name": "subtitlesStorageKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pt'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "Video_externalProviderId_key": { + "name": "Video_externalProviderId_key", + "columns": [ + "externalProviderId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Video_uploadBatchId_UploadBatch_id_fk": { + "name": "Video_uploadBatchId_UploadBatch_id_fk", + "tableFrom": "Video", + "tableTo": "UploadBatch", + "columnsFrom": [ + "uploadBatchId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Video_companyId_Company_id_fk": { + "name": "Video_companyId_Company_id_fk", + "tableFrom": "Video", + "tableTo": "Company", + "columnsFrom": [ + "companyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Webhook": { + "name": "Webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "videoId": { + "name": "videoId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "WebhookType", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "WebhookStatus", + "primaryKey": false, + "notNull": true, + "default": "'RUNNING'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finishedAt": { + "name": "finishedAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Webhook_videoId_Video_id_fk": { + "name": "Webhook_videoId_Video_id_fk", + "tableFrom": "Webhook", + "tableTo": "Video", + "columnsFrom": [ + "videoId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": { + "WebhookStatus": { + "name": "WebhookStatus", + "values": { + "ERROR": "ERROR", + "SUCCESS": "SUCCESS", + "RUNNING": "RUNNING" + } + }, + "WebhookType": { + "name": "WebhookType", + "values": { + "CREATE_SUBTITLES_FROM_TRANSCRIPTION": "CREATE_SUBTITLES_FROM_TRANSCRIPTION", + "UPDATE_EXTERNAL_PROVIDER_STATUS": "UPDATE_EXTERNAL_PROVIDER_STATUS", + "UPLOAD_TO_EXTERNAL_PROVIDER": "UPLOAD_TO_EXTERNAL_PROVIDER", + "CREATE_TRANSCRIPTION": "CREATE_TRANSCRIPTION", + "PROCESS_VIDEO": "PROCESS_VIDEO" + } + } + }, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..59b7302 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,20 @@ +{ + "version": "5", + "dialect": "pg", + "entries": [ + { + "idx": 0, + "version": "5", + "when": 1706875114570, + "tag": "0000_cute_obadiah_stane", + "breakpoints": true + }, + { + "idx": 1, + "version": "5", + "when": 1706875127486, + "tag": "0001_massive_jack_murdock", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index d62dd29..da70cae 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@ffmpeg/ffmpeg": "^0.12.10", "@ffmpeg/util": "^0.12.1", "@hookform/resolvers": "3.3.4", + "@neondatabase/serverless": "^0.7.2", "@prisma/client": "5.9.0", "@radix-ui/react-alert-dialog": "1.0.4", "@radix-ui/react-avatar": "1.0.3", @@ -45,6 +46,7 @@ "axios": "1.4.0", "cmdk": "0.2.0", "dayjs": "1.11.10", + "drizzle-orm": "^0.29.3", "elysia": "^0.8.15", "form-data": "4.0.0", "immer": "10.0.3", @@ -56,6 +58,7 @@ "next-themes": "0.2.1", "node-webvtt": "1.9.4", "openai-edge": "1.2.2", + "postgres": "^3.4.3", "react": "18.2.0", "react-dom": "18.2.0", "react-dropzone": "14.2.3", @@ -74,14 +77,17 @@ "@types/recharts": "1.8.24", "@types/ws": "8.5.5", "autoprefixer": "10.4.17", + "drizzle-kit": "^0.20.14", "eslint": "8.44.0", "eslint-config-next": "14.1.0", "eslint-plugin-simple-import-sort": "10.0.0", + "pg": "^8.11.3", "postcss": "8.4.33", "prettier-plugin-tailwindcss": "0.4.0", "prisma": "5.9.0", "tailwindcss": "3.4.1", "tailwindcss-animate": "1.0.6", + "tsx": "^4.7.0", "typescript": "5.3.3", "webpack-filter-warnings-plugin": "1.2.1" } diff --git a/prisma/migrations/20230710193825_create_base_tables/migration.sql b/prisma/migrations/20230710193825_create_base_tables/migration.sql deleted file mode 100644 index d7d2724..0000000 --- a/prisma/migrations/20230710193825_create_base_tables/migration.sql +++ /dev/null @@ -1,53 +0,0 @@ --- CreateTable -CREATE TABLE "Video" ( - "id" TEXT NOT NULL, - "duration" INTEGER, - "title" TEXT NOT NULL, - "storageKey" TEXT NOT NULL, - "description" TEXT, - "uploadBatchId" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "Video_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "UploadBatch" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "UploadBatch_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Tag" ( - "id" TEXT NOT NULL, - "slug" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "Tag_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "_TagToVideo" ( - "A" TEXT NOT NULL, - "B" TEXT NOT NULL -); - --- CreateIndex -CREATE UNIQUE INDEX "Tag_slug_key" ON "Tag"("slug"); - --- CreateIndex -CREATE UNIQUE INDEX "_TagToVideo_AB_unique" ON "_TagToVideo"("A", "B"); - --- CreateIndex -CREATE INDEX "_TagToVideo_B_index" ON "_TagToVideo"("B"); - --- AddForeignKey -ALTER TABLE "Video" ADD CONSTRAINT "Video_uploadBatchId_fkey" FOREIGN KEY ("uploadBatchId") REFERENCES "UploadBatch"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_TagToVideo" ADD CONSTRAINT "_TagToVideo_A_fkey" FOREIGN KEY ("A") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_TagToVideo" ADD CONSTRAINT "_TagToVideo_B_fkey" FOREIGN KEY ("B") REFERENCES "Video"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20230710220026_alter_duration_to_not_nullable/migration.sql b/prisma/migrations/20230710220026_alter_duration_to_not_nullable/migration.sql deleted file mode 100644 index 299ede9..0000000 --- a/prisma/migrations/20230710220026_alter_duration_to_not_nullable/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - Made the column `duration` on table `Video` required. This step will fail if there are existing NULL values in that column. - -*/ --- AlterTable -ALTER TABLE "Video" ALTER COLUMN "duration" SET NOT NULL; diff --git a/prisma/migrations/20230710221828_add_external_provider_id_to_videos/migration.sql b/prisma/migrations/20230710221828_add_external_provider_id_to_videos/migration.sql deleted file mode 100644 index 075306a..0000000 --- a/prisma/migrations/20230710221828_add_external_provider_id_to_videos/migration.sql +++ /dev/null @@ -1,11 +0,0 @@ -/* - Warnings: - - - A unique constraint covering the columns `[externalProviderId]` on the table `Video` will be added. If there are existing duplicate values, this will fail. - -*/ --- AlterTable -ALTER TABLE "Video" ADD COLUMN "externalProviderId" TEXT; - --- CreateIndex -CREATE UNIQUE INDEX "Video_externalProviderId_key" ON "Video"("externalProviderId"); diff --git a/prisma/migrations/20230711181144_add_audio_storage_key/migration.sql b/prisma/migrations/20230711181144_add_audio_storage_key/migration.sql deleted file mode 100644 index f67dbb6..0000000 --- a/prisma/migrations/20230711181144_add_audio_storage_key/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - Added the required column `audioStorageKey` to the `Video` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE "Video" ADD COLUMN "audioStorageKey" TEXT NOT NULL; diff --git a/prisma/migrations/20230711183118_add_transcription_and_processed_at_fields_to_video/migration.sql b/prisma/migrations/20230711183118_add_transcription_and_processed_at_fields_to_video/migration.sql deleted file mode 100644 index 802f226..0000000 --- a/prisma/migrations/20230711183118_add_transcription_and_processed_at_fields_to_video/migration.sql +++ /dev/null @@ -1,3 +0,0 @@ --- AlterTable -ALTER TABLE "Video" ADD COLUMN "processedAt" TIMESTAMP(3), -ADD COLUMN "transcription" TEXT; diff --git a/prisma/migrations/20230711194053_create_transcription/migration.sql b/prisma/migrations/20230711194053_create_transcription/migration.sql deleted file mode 100644 index ea6477c..0000000 --- a/prisma/migrations/20230711194053_create_transcription/migration.sql +++ /dev/null @@ -1,23 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `transcription` on the `Video` table. All the data in the column will be lost. - -*/ --- AlterTable -ALTER TABLE "Video" DROP COLUMN "transcription"; - --- CreateTable -CREATE TABLE "Transcription" ( - "id" TEXT NOT NULL, - "videoId" TEXT NOT NULL, - "text" TEXT NOT NULL, - - CONSTRAINT "Transcription_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "Transcription_videoId_key" ON "Transcription"("videoId"); - --- AddForeignKey -ALTER TABLE "Transcription" ADD CONSTRAINT "Transcription_videoId_fkey" FOREIGN KEY ("videoId") REFERENCES "Video"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20230712011227_create_transcription_segments/migration.sql b/prisma/migrations/20230712011227_create_transcription_segments/migration.sql deleted file mode 100644 index d2a994f..0000000 --- a/prisma/migrations/20230712011227_create_transcription_segments/migration.sql +++ /dev/null @@ -1,16 +0,0 @@ --- AlterTable -ALTER TABLE "Transcription" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; - --- CreateTable -CREATE TABLE "TranscriptionSegment" ( - "id" TEXT NOT NULL, - "transcriptionId" TEXT NOT NULL, - "start" DECIMAL(65,30) NOT NULL, - "end" DECIMAL(65,30) NOT NULL, - "text" TEXT NOT NULL, - - CONSTRAINT "TranscriptionSegment_pkey" PRIMARY KEY ("id") -); - --- AddForeignKey -ALTER TABLE "TranscriptionSegment" ADD CONSTRAINT "TranscriptionSegment_transcriptionId_fkey" FOREIGN KEY ("transcriptionId") REFERENCES "Transcription"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20230712011745_change_decimal_range/migration.sql b/prisma/migrations/20230712011745_change_decimal_range/migration.sql deleted file mode 100644 index 0466054..0000000 --- a/prisma/migrations/20230712011745_change_decimal_range/migration.sql +++ /dev/null @@ -1,10 +0,0 @@ -/* - Warnings: - - - You are about to alter the column `start` on the `TranscriptionSegment` table. The data in that column could be lost. The data in that column will be cast from `Decimal(65,30)` to `Decimal(10,2)`. - - You are about to alter the column `end` on the `TranscriptionSegment` table. The data in that column could be lost. The data in that column will be cast from `Decimal(65,30)` to `Decimal(10,2)`. - -*/ --- AlterTable -ALTER TABLE "TranscriptionSegment" ALTER COLUMN "start" SET DATA TYPE DECIMAL(10,2), -ALTER COLUMN "end" SET DATA TYPE DECIMAL(10,2); diff --git a/prisma/migrations/20230713020533_add_size_in_bytes_to_videos/migration.sql b/prisma/migrations/20230713020533_add_size_in_bytes_to_videos/migration.sql deleted file mode 100644 index d96d6ce..0000000 --- a/prisma/migrations/20230713020533_add_size_in_bytes_to_videos/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - Added the required column `sizeInBytes` to the `Video` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE "Video" ADD COLUMN "sizeInBytes" INTEGER NOT NULL; diff --git a/prisma/migrations/20230713024123_add_upload_order/migration.sql b/prisma/migrations/20230713024123_add_upload_order/migration.sql deleted file mode 100644 index af3a3eb..0000000 --- a/prisma/migrations/20230713024123_add_upload_order/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - Added the required column `uploadOrder` to the `Video` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE "Video" ADD COLUMN "uploadOrder" INTEGER NOT NULL; diff --git a/prisma/migrations/20230713151613_add_reviewed_at_to_transcriptions/migration.sql b/prisma/migrations/20230713151613_add_reviewed_at_to_transcriptions/migration.sql deleted file mode 100644 index bcafc59..0000000 --- a/prisma/migrations/20230713151613_add_reviewed_at_to_transcriptions/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "Transcription" ADD COLUMN "reviewedAt" TIMESTAMP(3); diff --git a/prisma/migrations/20230713151720_add_reviewed_at_to_transcriptions/migration.sql b/prisma/migrations/20230713151720_add_reviewed_at_to_transcriptions/migration.sql deleted file mode 100644 index 7a72497..0000000 --- a/prisma/migrations/20230713151720_add_reviewed_at_to_transcriptions/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `text` on the `Transcription` table. All the data in the column will be lost. - -*/ --- AlterTable -ALTER TABLE "Transcription" DROP COLUMN "text"; diff --git a/prisma/migrations/20230713204140_test_column/migration.sql b/prisma/migrations/20230713204140_test_column/migration.sql deleted file mode 100644 index 3627e72..0000000 --- a/prisma/migrations/20230713204140_test_column/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "Video" ADD COLUMN "newTestColumn" TEXT; diff --git a/prisma/migrations/20230715225245_create_webhooks/migration.sql b/prisma/migrations/20230715225245_create_webhooks/migration.sql deleted file mode 100644 index 0754fd9..0000000 --- a/prisma/migrations/20230715225245_create_webhooks/migration.sql +++ /dev/null @@ -1,30 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `newTestColumn` on the `Video` table. All the data in the column will be lost. - -*/ --- CreateEnum -CREATE TYPE "WebhookType" AS ENUM ('PROCESS_VIDEO', 'CREATE_TRANSCRIPTION', 'UPLOAD_TO_EXTERNAL_PROVIDER'); - --- CreateEnum -CREATE TYPE "WebhookStatus" AS ENUM ('RUNNING', 'SUCCESS', 'ERROR'); - --- AlterTable -ALTER TABLE "Video" DROP COLUMN "newTestColumn"; - --- CreateTable -CREATE TABLE "Webhook" ( - "id" TEXT NOT NULL, - "videoId" TEXT NOT NULL, - "type" "WebhookType" NOT NULL, - "status" "WebhookStatus" NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "finishedAt" TIMESTAMP(3), - "metadata" TEXT, - - CONSTRAINT "Webhook_pkey" PRIMARY KEY ("id") -); - --- AddForeignKey -ALTER TABLE "Webhook" ADD CONSTRAINT "Webhook_videoId_fkey" FOREIGN KEY ("videoId") REFERENCES "Video"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20230715225353_alter_webhook_status_default_value/migration.sql b/prisma/migrations/20230715225353_alter_webhook_status_default_value/migration.sql deleted file mode 100644 index 6369114..0000000 --- a/prisma/migrations/20230715225353_alter_webhook_status_default_value/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "Webhook" ALTER COLUMN "status" SET DEFAULT 'RUNNING'; diff --git a/prisma/migrations/20230715230658_add_update_external_provider_status_webhook_type/migration.sql b/prisma/migrations/20230715230658_add_update_external_provider_status_webhook_type/migration.sql deleted file mode 100644 index aef1942..0000000 --- a/prisma/migrations/20230715230658_add_update_external_provider_status_webhook_type/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterEnum -ALTER TYPE "WebhookType" ADD VALUE 'UPDATE_EXTERNAL_PROVIDER_STATUS'; diff --git a/prisma/migrations/20230719205748_add_commit_url_to_videos/migration.sql b/prisma/migrations/20230719205748_add_commit_url_to_videos/migration.sql deleted file mode 100644 index 0e6c670..0000000 --- a/prisma/migrations/20230719205748_add_commit_url_to_videos/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "Video" ADD COLUMN "commitUrl" TEXT; diff --git a/prisma/migrations/20230724165750_create_new_webhook_type/migration.sql b/prisma/migrations/20230724165750_create_new_webhook_type/migration.sql deleted file mode 100644 index 33a12b8..0000000 --- a/prisma/migrations/20230724165750_create_new_webhook_type/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterEnum -ALTER TYPE "WebhookType" ADD VALUE 'CREATE_SUBTITLES_FROM_TRANSCRIPTION'; diff --git a/prisma/migrations/20230724165941_add_subtitles_storage_key_to_videos/migration.sql b/prisma/migrations/20230724165941_add_subtitles_storage_key_to_videos/migration.sql deleted file mode 100644 index ee16bf7..0000000 --- a/prisma/migrations/20230724165941_add_subtitles_storage_key_to_videos/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "Video" ADD COLUMN "subtitlesStorageKey" TEXT; diff --git a/prisma/migrations/20230726142319_add_cascade_to_relations/migration.sql b/prisma/migrations/20230726142319_add_cascade_to_relations/migration.sql deleted file mode 100644 index d0f26e0..0000000 --- a/prisma/migrations/20230726142319_add_cascade_to_relations/migration.sql +++ /dev/null @@ -1,26 +0,0 @@ --- DropForeignKey -ALTER TABLE "Transcription" DROP CONSTRAINT "Transcription_videoId_fkey"; - --- DropForeignKey -ALTER TABLE "TranscriptionSegment" DROP CONSTRAINT "TranscriptionSegment_transcriptionId_fkey"; - --- DropForeignKey -ALTER TABLE "Video" DROP CONSTRAINT "Video_uploadBatchId_fkey"; - --- DropForeignKey -ALTER TABLE "Webhook" DROP CONSTRAINT "Webhook_videoId_fkey"; - --- AlterTable -ALTER TABLE "Video" ALTER COLUMN "uploadBatchId" DROP NOT NULL; - --- AddForeignKey -ALTER TABLE "Video" ADD CONSTRAINT "Video_uploadBatchId_fkey" FOREIGN KEY ("uploadBatchId") REFERENCES "UploadBatch"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Webhook" ADD CONSTRAINT "Webhook_videoId_fkey" FOREIGN KEY ("videoId") REFERENCES "Video"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Transcription" ADD CONSTRAINT "Transcription_videoId_fkey" FOREIGN KEY ("videoId") REFERENCES "Video"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "TranscriptionSegment" ADD CONSTRAINT "TranscriptionSegment_transcriptionId_fkey" FOREIGN KEY ("transcriptionId") REFERENCES "Transcription"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20230730162726_allow_null_storage_or_audio_storage_keys/migration.sql b/prisma/migrations/20230730162726_allow_null_storage_or_audio_storage_keys/migration.sql deleted file mode 100644 index e126a9d..0000000 --- a/prisma/migrations/20230730162726_allow_null_storage_or_audio_storage_keys/migration.sql +++ /dev/null @@ -1,3 +0,0 @@ --- AlterTable -ALTER TABLE "Video" ALTER COLUMN "storageKey" DROP NOT NULL, -ALTER COLUMN "audioStorageKey" DROP NOT NULL; diff --git a/prisma/migrations/20240123190935_add_language_to_videos/migration.sql b/prisma/migrations/20240123190935_add_language_to_videos/migration.sql deleted file mode 100644 index 20e17c3..0000000 --- a/prisma/migrations/20240123190935_add_language_to_videos/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "Video" ADD COLUMN "language" TEXT NOT NULL DEFAULT 'pt'; diff --git a/prisma/migrations/20240201182601_create_companies/migration.sql b/prisma/migrations/20240201182601_create_companies/migration.sql deleted file mode 100644 index 3338185..0000000 --- a/prisma/migrations/20240201182601_create_companies/migration.sql +++ /dev/null @@ -1,14 +0,0 @@ --- CreateTable -CREATE TABLE "Company" ( - "id" TEXT NOT NULL, - "name" TEXT NOT NULL, - "domain" TEXT NOT NULL, - - CONSTRAINT "Company_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "Company_domain_key" ON "Company"("domain"); - -INSERT INTO "Company" ("id", "name", "domain") -VALUES ('BE7CB85C-E3A4-4B37-908C-400C1A582749', 'Rocketseat', 'rocketseat.team'); \ No newline at end of file diff --git a/prisma/migrations/20240201182915_add_company_id_reference_to_tables/migration.sql b/prisma/migrations/20240201182915_add_company_id_reference_to_tables/migration.sql deleted file mode 100644 index e3e93f5..0000000 --- a/prisma/migrations/20240201182915_add_company_id_reference_to_tables/migration.sql +++ /dev/null @@ -1,25 +0,0 @@ --- AlterTable -ALTER TABLE "Tag" ADD COLUMN "companyId" TEXT; - --- AlterTable -ALTER TABLE "UploadBatch" ADD COLUMN "companyId" TEXT; - --- AlterTable -ALTER TABLE "Video" ADD COLUMN "companyId" TEXT; - --- AddForeignKey -ALTER TABLE "Video" ADD CONSTRAINT "Video_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "UploadBatch" ADD CONSTRAINT "UploadBatch_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Tag" ADD CONSTRAINT "Tag_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- Insert default values - -UPDATE "Tag" SET "companyId" = 'BE7CB85C-E3A4-4B37-908C-400C1A582749' WHERE "companyId" IS NULL; - -UPDATE "UploadBatch" SET "companyId" = 'BE7CB85C-E3A4-4B37-908C-400C1A582749' WHERE "companyId" IS NULL; - -UPDATE "Video" SET "companyId" = 'BE7CB85C-E3A4-4B37-908C-400C1A582749' WHERE "companyId" IS NULL; \ No newline at end of file diff --git a/prisma/migrations/20240201183144_set_company_id_as_not_null/migration.sql b/prisma/migrations/20240201183144_set_company_id_as_not_null/migration.sql deleted file mode 100644 index d86a96c..0000000 --- a/prisma/migrations/20240201183144_set_company_id_as_not_null/migration.sql +++ /dev/null @@ -1,34 +0,0 @@ -/* - Warnings: - - - Made the column `companyId` on table `Tag` required. This step will fail if there are existing NULL values in that column. - - Made the column `companyId` on table `UploadBatch` required. This step will fail if there are existing NULL values in that column. - - Made the column `companyId` on table `Video` required. This step will fail if there are existing NULL values in that column. - -*/ --- DropForeignKey -ALTER TABLE "Tag" DROP CONSTRAINT "Tag_companyId_fkey"; - --- DropForeignKey -ALTER TABLE "UploadBatch" DROP CONSTRAINT "UploadBatch_companyId_fkey"; - --- DropForeignKey -ALTER TABLE "Video" DROP CONSTRAINT "Video_companyId_fkey"; - --- AlterTable -ALTER TABLE "Tag" ALTER COLUMN "companyId" SET NOT NULL; - --- AlterTable -ALTER TABLE "UploadBatch" ALTER COLUMN "companyId" SET NOT NULL; - --- AlterTable -ALTER TABLE "Video" ALTER COLUMN "companyId" SET NOT NULL; - --- AddForeignKey -ALTER TABLE "Video" ADD CONSTRAINT "Video_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "UploadBatch" ADD CONSTRAINT "UploadBatch_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Tag" ADD CONSTRAINT "Tag_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20240201190211_users_and_sessions_structure/migration.sql b/prisma/migrations/20240201190211_users_and_sessions_structure/migration.sql deleted file mode 100644 index fc4aadf..0000000 --- a/prisma/migrations/20240201190211_users_and_sessions_structure/migration.sql +++ /dev/null @@ -1,70 +0,0 @@ --- CreateTable -CREATE TABLE "Account" ( - "id" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "type" TEXT NOT NULL, - "provider" TEXT NOT NULL, - "providerAccountId" TEXT NOT NULL, - "refresh_token" TEXT, - "access_token" TEXT, - "expires_at" INTEGER, - "token_type" TEXT, - "scope" TEXT, - "id_token" TEXT, - "session_state" TEXT, - - CONSTRAINT "Account_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Session" ( - "id" TEXT NOT NULL, - "sessionToken" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "expires" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Session_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "User" ( - "id" TEXT NOT NULL, - "companyId" TEXT NOT NULL, - "name" TEXT, - "email" TEXT, - "emailVerified" TIMESTAMP(3), - "image" TEXT, - - CONSTRAINT "User_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "VerificationToken" ( - "identifier" TEXT NOT NULL, - "token" TEXT NOT NULL, - "expires" TIMESTAMP(3) NOT NULL -); - --- CreateIndex -CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); - --- CreateIndex -CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); - --- CreateIndex -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); - --- CreateIndex -CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token"); - --- CreateIndex -CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token"); - --- AddForeignKey -ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "User" ADD CONSTRAINT "User_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20240201191323_alter_email_to_not_null/migration.sql b/prisma/migrations/20240201191323_alter_email_to_not_null/migration.sql deleted file mode 100644 index 87d34ff..0000000 --- a/prisma/migrations/20240201191323_alter_email_to_not_null/migration.sql +++ /dev/null @@ -1,25 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `access_token` on the `Account` table. All the data in the column will be lost. - - You are about to drop the column `expires_at` on the `Account` table. All the data in the column will be lost. - - You are about to drop the column `id_token` on the `Account` table. All the data in the column will be lost. - - You are about to drop the column `refresh_token` on the `Account` table. All the data in the column will be lost. - - You are about to drop the column `session_state` on the `Account` table. All the data in the column will be lost. - - Made the column `email` on table `User` required. This step will fail if there are existing NULL values in that column. - -*/ --- AlterTable -ALTER TABLE "Account" DROP COLUMN "access_token", -DROP COLUMN "expires_at", -DROP COLUMN "id_token", -DROP COLUMN "refresh_token", -DROP COLUMN "session_state", -ADD COLUMN "accessToken" TEXT, -ADD COLUMN "expiresAt" INTEGER, -ADD COLUMN "idToken" TEXT, -ADD COLUMN "refreshToken" TEXT, -ADD COLUMN "sessionState" TEXT; - --- AlterTable -ALTER TABLE "User" ALTER COLUMN "email" SET NOT NULL; diff --git a/prisma/migrations/20240201192212_adjust_field_casing/migration.sql b/prisma/migrations/20240201192212_adjust_field_casing/migration.sql deleted file mode 100644 index 575acc2..0000000 --- a/prisma/migrations/20240201192212_adjust_field_casing/migration.sql +++ /dev/null @@ -1,9 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `token_type` on the `Account` table. All the data in the column will be lost. - -*/ --- AlterTable -ALTER TABLE "Account" DROP COLUMN "token_type", -ADD COLUMN "tokenType" TEXT; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml deleted file mode 100644 index fbffa92..0000000 --- a/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) -provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma deleted file mode 100644 index 55531bb..0000000 --- a/prisma/schema.prisma +++ /dev/null @@ -1,161 +0,0 @@ -generator client { - provider = "prisma-client-js" - previewFeatures = ["fullTextSearch"] - binaryTargets = ["native", "rhel-openssl-1.0.x"] -} - -datasource db { - provider = "postgres" - url = env("DATABASE_URL") - directUrl = env("DIRECT_DATABASE_URL") -} - -model Account { - id String @id @default(uuid()) - userId String - type String - provider String - providerAccountId String - refreshToken String? - accessToken String? - expiresAt Int? - tokenType String? - scope String? - idToken String? - sessionState String? - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@unique([provider, providerAccountId]) -} - -model Session { - id String @id @default(uuid()) - sessionToken String @unique - userId String - expires DateTime - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) -} - -model User { - id String @id @default(uuid()) - companyId String - name String? - email String @unique - emailVerified DateTime? - image String? - - company Company @relation(fields: [companyId], references: [id]) - accounts Account[] - sessions Session[] -} - -model VerificationToken { - identifier String - token String @unique - expires DateTime - - @@unique([identifier, token]) -} - -model Company { - id String @id @default(uuid()) - name String - domain String @unique - - videos Video[] - tags Tag[] - batches UploadBatch[] - members User[] -} - -model Video { - id String @id @default(uuid()) - uploadBatchId String? - companyId String - language String @default("pt") - uploadOrder Int - duration Int - sizeInBytes Int - title String - storageKey String? - audioStorageKey String? - subtitlesStorageKey String? - description String? - externalProviderId String? @unique - processedAt DateTime? - commitUrl String? - createdAt DateTime @default(now()) - - tags Tag[] - uploadBatch UploadBatch? @relation(fields: [uploadBatchId], references: [id], onDelete: SetNull) - transcription Transcription? - webhooks Webhook[] - company Company @relation(fields: [companyId], references: [id]) -} - -enum WebhookType { - PROCESS_VIDEO - CREATE_TRANSCRIPTION - UPLOAD_TO_EXTERNAL_PROVIDER - UPDATE_EXTERNAL_PROVIDER_STATUS - CREATE_SUBTITLES_FROM_TRANSCRIPTION -} - -enum WebhookStatus { - RUNNING - SUCCESS - ERROR -} - -model Webhook { - id String @id @default(uuid()) - videoId String - type WebhookType - status WebhookStatus @default(RUNNING) - createdAt DateTime @default(now()) - finishedAt DateTime? - metadata String? - - video Video @relation(fields: [videoId], references: [id], onDelete: Cascade) -} - -model Transcription { - id String @id @default(uuid()) - videoId String @unique - reviewedAt DateTime? - createdAt DateTime @default(now()) - - video Video @relation(fields: [videoId], references: [id], onDelete: Cascade) - segments TranscriptionSegment[] -} - -model TranscriptionSegment { - id String @id @default(uuid()) - transcriptionId String - start Decimal @db.Decimal(10, 2) - end Decimal @db.Decimal(10, 2) - text String - - transcription Transcription @relation(fields: [transcriptionId], references: [id], onDelete: Cascade) -} - -model UploadBatch { - id String @id @default(uuid()) - companyId String - createdAt DateTime @default(now()) - - videos Video[] - company Company @relation(fields: [companyId], references: [id]) -} - -model Tag { - id String @id @default(uuid()) - companyId String - slug String @unique - createdAt DateTime @default(now()) - - videos Video[] - company Company @relation(fields: [companyId], references: [id]) -} diff --git a/server/app.ts b/server/app.ts index 92f4421..773cc17 100644 --- a/server/app.ts +++ b/server/app.ts @@ -7,6 +7,7 @@ import { downloadUploadMedia } from './routes/download-upload-media' import { generateAIDescription } from './routes/generate-ai-description' import { generateAITitle } from './routes/generate-ai-title' import { getTags } from './routes/get-tags' +import { getUpload } from './routes/get-upload' import { getUploadBatch } from './routes/get-upload-batch' import { getUploadTranscription } from './routes/get-upload-transcription' import { getUploadWebhooks } from './routes/get-upload-webhooks' @@ -14,7 +15,6 @@ import { getUploads } from './routes/get-uploads' import { requestAudioUploadURL } from './routes/request-audio-upload-url' import { requestVideoUploadURL } from './routes/request-video-upload-url' import { updateUpload } from './routes/update-upload' -import { updateUploadTranscription } from './routes/update-upload-transcription' export const app = new Elysia({ prefix: '/api' }) .use(createTag) @@ -28,9 +28,16 @@ export const app = new Elysia({ prefix: '/api' }) .use(getUploadTranscription) .use(getUploadWebhooks) .use(getUploads) + .use(getUpload) .use(requestAudioUploadURL) .use(requestVideoUploadURL) - .use(updateUploadTranscription) .use(updateUpload) + .onError(({ error, set }) => { + console.error(error) + + set.status = 500 + + return new Response() + }) export type App = typeof app diff --git a/server/routes/create-tag.ts b/server/routes/create-tag.ts index 64ad25d..da11cd7 100644 --- a/server/routes/create-tag.ts +++ b/server/routes/create-tag.ts @@ -1,16 +1,16 @@ import { Elysia, t } from 'elysia' -import { prisma } from '@/lib/prisma' +import { db } from '@/drizzle/client' +import { tag } from '@/drizzle/schema' export const createTag = new Elysia().post( '/tags', async ({ body }) => { - const { tag } = body + const { tag: slug } = body - await prisma.tag.create({ - data: { - slug: tag, - }, + await db.insert(tag).values({ + slug, + companyId: 'ae6780ef-b2c2-4041-bada-c48e27ac6157', }) return new Response(null, { status: 201 }) diff --git a/server/routes/create-upload-batch.ts b/server/routes/create-upload-batch.ts index f44bef0..f2046a1 100644 --- a/server/routes/create-upload-batch.ts +++ b/server/routes/create-upload-batch.ts @@ -1,9 +1,8 @@ -import { randomUUID } from 'node:crypto' - import { Elysia, t } from 'elysia' +import { db } from '@/drizzle/client' +import { tagToVideo, uploadBatch, video } from '@/drizzle/schema' import { publishMessagesOnTopic } from '@/lib/kafka' -import { prisma } from '@/lib/prisma' import { publishMessage } from '@/lib/qstash' export const createUploadBatch = new Elysia().post( @@ -11,35 +10,62 @@ export const createUploadBatch = new Elysia().post( async ({ body, set }) => { const { files: videos } = body - const batchId = randomUUID() - - await prisma.$transaction(async (tx) => { - await tx.uploadBatch.create({ - data: { id: batchId }, - }) + const { batchId } = await db.transaction(async (tx) => { + const [{ id: batchId }] = await tx + .insert(uploadBatch) + .values({ + companyId: 'ae6780ef-b2c2-4041-bada-c48e27ac6157', + }) + .returning({ + id: uploadBatch.id, + }) - await Promise.all( - videos.map((video, index) => { - return tx.video.create({ - data: { - id: video.id, - language: video.language, - uploadBatchId: batchId, - uploadOrder: index + 1, - title: video.title, - sizeInBytes: video.sizeInBytes, - duration: video.duration, - tags: { - connect: video.tags.map((tag) => { - return { - slug: tag, - } - }), - }, - }, - }) + await tx.insert(video).values( + videos.map((videoItem, index) => { + return { + id: videoItem.id, + language: videoItem.language, + uploadBatchId: batchId, + uploadOrder: index + 1, + title: videoItem.title, + sizeInBytes: videoItem.sizeInBytes, + duration: videoItem.duration, + companyId: 'ae6780ef-b2c2-4041-bada-c48e27ac6157', + } }), ) + + const tagsOnVideos = await tx.query.tag.findMany({ + where(fields, { inArray }) { + return inArray( + fields.slug, + videos.flatMap((videoItem) => videoItem.tags), + ) + }, + }) + + const tagSlugToId = tagsOnVideos.reduce((map, item) => { + return map.set(item.slug, item.id) + }, new Map()) + + const tagToVideos = videos.flatMap((videoItem) => { + return videoItem.tags.map((videoTag) => { + const tagId = tagSlugToId.get(videoTag) + + if (!tagId) { + throw new Error(`Tag with slug "${videoTag}" was not found.`) + } + + return { + a: tagId, + b: videoItem.id, + } + }) + }) + + await tx.insert(tagToVideo).values(tagToVideos) + + return { batchId } }) await Promise.all( diff --git a/server/routes/delete-upload.ts b/server/routes/delete-upload.ts index 6dd9fcf..9e02131 100644 --- a/server/routes/delete-upload.ts +++ b/server/routes/delete-upload.ts @@ -1,11 +1,13 @@ import { DeleteObjectsCommand, ObjectIdentifier } from '@aws-sdk/client-s3' import axios from 'axios' +import { eq } from 'drizzle-orm' import { Elysia, t } from 'elysia' +import { db } from '@/drizzle/client' +import { video } from '@/drizzle/schema' import { env } from '@/env' import { r2 } from '@/lib/cloudflare-r2' import { publishMessagesOnTopic } from '@/lib/kafka' -import { prisma } from '@/lib/prisma' import { publishMessage } from '@/lib/qstash' export const deleteUpload = new Elysia().delete( @@ -13,30 +15,34 @@ export const deleteUpload = new Elysia().delete( async ({ params }) => { const { videoId } = params - const video = await prisma.video.findUniqueOrThrow({ - where: { - id: videoId, + const videoToDelete = await db.query.video.findFirst({ + where(fields, { eq }) { + return eq(fields.id, videoId) }, }) + if (!videoToDelete) { + throw new Error('Video not found.') + } + const objectsToDelete: ObjectIdentifier[] = [] const deletionPromises: Promise[] = [] - if (video.storageKey) { + if (videoToDelete.storageKey) { objectsToDelete.push({ - Key: video.storageKey, + Key: videoToDelete.storageKey, }) } - if (video.audioStorageKey) { + if (videoToDelete.audioStorageKey) { objectsToDelete.push({ - Key: video.audioStorageKey, + Key: videoToDelete.audioStorageKey, }) } - if (video.subtitlesStorageKey) { + if (videoToDelete.subtitlesStorageKey) { objectsToDelete.push({ - Key: video.subtitlesStorageKey, + Key: videoToDelete.subtitlesStorageKey, }) } @@ -54,7 +60,7 @@ export const deleteUpload = new Elysia().delete( ) } - if (video.externalProviderId) { + if (videoToDelete.externalProviderId) { deletionPromises.push( axios.delete('https://api-v2.pandavideo.com.br/videos', { data: [{ video_id: videoId }], @@ -65,13 +71,7 @@ export const deleteUpload = new Elysia().delete( ) } - deletionPromises.push( - prisma.video.delete({ - where: { - id: videoId, - }, - }), - ) + deletionPromises.push(db.delete(video).where(eq(video.id, videoId))) await Promise.all(deletionPromises) diff --git a/server/routes/download-upload-media.ts b/server/routes/download-upload-media.ts index 64f47e5..4067ee2 100644 --- a/server/routes/download-upload-media.ts +++ b/server/routes/download-upload-media.ts @@ -2,24 +2,28 @@ import { GetObjectCommand } from '@aws-sdk/client-s3' import { getSignedUrl } from '@aws-sdk/s3-request-presigner' import { Elysia, t } from 'elysia' +import { db } from '@/drizzle/client' import { env } from '@/env' import { r2 } from '@/lib/cloudflare-r2' -import { prisma } from '@/lib/prisma' export const downloadUploadMedia = new Elysia().get( '/videos/:videoId/download/:media', async ({ params, set }) => { const { videoId, media } = params - const video = await prisma.video.findUniqueOrThrow({ - where: { - id: videoId, + const videoToDownload = await db.query.video.findFirst({ + where(fields, { eq }) { + return eq(fields.id, videoId) }, }) + if (!videoToDownload) { + throw new Error('Video not found.') + } + const columnKey = media === 'video' ? 'storageKey' : 'audioStorageKey' - const downloadKey = video[columnKey] + const downloadKey = videoToDownload[columnKey] if (downloadKey === null) { set.status = 400 diff --git a/server/routes/generate-ai-description.ts b/server/routes/generate-ai-description.ts index ce84cdf..9e9b1bb 100644 --- a/server/routes/generate-ai-description.ts +++ b/server/routes/generate-ai-description.ts @@ -1,25 +1,28 @@ import { OpenAIStream, StreamingTextResponse } from 'ai' +import { eq, sql } from 'drizzle-orm' import { Elysia, t } from 'elysia' +import { db } from '@/drizzle/client' +import { transcription, transcriptionSegment } from '@/drizzle/schema' import { openai } from '@/lib/openai' -import { prisma } from '@/lib/prisma' export const generateAIDescription = new Elysia().post( '/ai/generate/description', async ({ set, query }) => { const { videoId } = query - const [transcription] = await prisma.$queryRaw< - [{ text: string }] - >/* sql */ ` - SELECT - STRING_AGG("public"."TranscriptionSegment"."text", '') as "text" - FROM "public"."TranscriptionSegment" - JOIN "public"."Transcription" ON "public"."Transcription"."id" = "public"."TranscriptionSegment"."transcriptionId" - WHERE "public"."Transcription"."videoId" = ${videoId} - ` + const [{ text }] = await db + .select({ + text: sql`STRING_AGG(${transcriptionSegment.text}, '')`, + }) + .from(transcriptionSegment) + .innerJoin( + transcription, + eq(transcription.id, transcriptionSegment.transcriptionId), + ) + .where(eq(transcription.videoId, videoId)) - if (!transcription.text) { + if (!text) { set.status = 400 return { message: 'Transcription not found.' } @@ -46,7 +49,7 @@ export const generateAIDescription = new Elysia().post( }, { role: 'user', - content: `Gere um resumo da transcrição abaixo. Retorne o resumo no mesmo idioma da transcrição. \n\n ${transcription.text}`, + content: `Gere um resumo da transcrição abaixo. Retorne o resumo no mesmo idioma da transcrição. \n\n ${text}`, }, ], temperature: 0, diff --git a/server/routes/get-tags.ts b/server/routes/get-tags.ts index 3727ac1..aeeaf1e 100644 --- a/server/routes/get-tags.ts +++ b/server/routes/get-tags.ts @@ -1,33 +1,36 @@ +import { count, ilike } from 'drizzle-orm' import { Elysia, t } from 'elysia' -import { prisma } from '@/lib/prisma' +import { db } from '@/drizzle/client' +import { tag } from '@/drizzle/schema' export const getTags = new Elysia().get( '/tags/search', async ({ query }) => { const { q: search, pageIndex, pageSize } = query - const slugSearch = search.length ? { contains: search } : undefined + const [tags, [{ amount }]] = await Promise.all([ + db.query.tag.findMany({ + where(fields, { ilike }) { + if (search) { + return ilike(fields.slug, `%${search}%`) + } - const [tags, count] = await Promise.all([ - prisma.tag.findMany({ - where: { - slug: slugSearch, + return undefined }, - skip: pageIndex * pageSize, - take: pageSize, - orderBy: { - createdAt: 'desc', - }, - }), - prisma.tag.count({ - where: { - slug: slugSearch, + offset: pageIndex * pageSize, + limit: pageSize, + orderBy(fields, { desc }) { + return desc(fields.createdAt) }, }), + db + .select({ amount: count() }) + .from(tag) + .where(search ? ilike(tag.slug, `%${search}%`) : undefined), ]) - const pageCount = Math.ceil(count / pageSize) ?? 0 + const pageCount = Math.ceil(amount / pageSize) ?? 0 return { tags, pageCount } }, diff --git a/server/routes/get-upload-batch.ts b/server/routes/get-upload-batch.ts index 9eedd0b..465291a 100644 --- a/server/routes/get-upload-batch.ts +++ b/server/routes/get-upload-batch.ts @@ -1,32 +1,36 @@ import { Elysia, t } from 'elysia' -import { prisma } from '@/lib/prisma' +import { db } from '@/drizzle/client' export const getUploadBatch = new Elysia().get( '/batches/:batchId', async ({ params }) => { const { batchId } = params - const batch = await prisma.uploadBatch.findUniqueOrThrow({ - where: { - id: batchId, + const batch = await db.query.uploadBatch.findFirst({ + where(fields, { eq }) { + return eq(fields.id, batchId) }, - include: { + with: { videos: { - include: { + with: { transcription: { - select: { + columns: { id: true, }, }, }, - orderBy: { - uploadOrder: 'asc', + orderBy(fields, { asc }) { + return asc(fields.uploadOrder) }, }, }, }) + if (!batch) { + throw new Error('Batch not found.') + } + return { batch } }, { diff --git a/server/routes/get-upload-transcription.ts b/server/routes/get-upload-transcription.ts index d6b1735..24ab6a7 100644 --- a/server/routes/get-upload-transcription.ts +++ b/server/routes/get-upload-transcription.ts @@ -1,20 +1,20 @@ import { Elysia, t } from 'elysia' -import { prisma } from '@/lib/prisma' +import { db } from '@/drizzle/client' export const getUploadTranscription = new Elysia().get( '/videos/:videoId/transcription', async ({ params, set }) => { const { videoId } = params - const transcription = await prisma.transcription.findUnique({ - where: { - videoId, + const transcription = await db.query.transcription.findFirst({ + where(fields, { eq }) { + return eq(fields.videoId, videoId) }, - include: { + with: { segments: { - orderBy: { - start: 'asc', + orderBy(fields, { asc }) { + return asc(fields.start) }, }, }, diff --git a/server/routes/get-upload-webhooks.ts b/server/routes/get-upload-webhooks.ts index 4253f99..dc1dc41 100644 --- a/server/routes/get-upload-webhooks.ts +++ b/server/routes/get-upload-webhooks.ts @@ -1,18 +1,18 @@ import { Elysia, t } from 'elysia' -import { prisma } from '@/lib/prisma' +import { db } from '@/drizzle/client' export const getUploadWebhooks = new Elysia().get( '/videos/:videoId/webhooks', async ({ params }) => { const { videoId } = params - const webhooks = await prisma.webhook.findMany({ - where: { - videoId, + const webhooks = await db.query.webhook.findMany({ + where(fields, { eq }) { + return eq(fields.videoId, videoId) }, - orderBy: { - createdAt: 'desc', + orderBy(fields, { desc }) { + return desc(fields.createdAt) }, }) diff --git a/server/routes/get-upload.ts b/server/routes/get-upload.ts new file mode 100644 index 0000000..7abd7bd --- /dev/null +++ b/server/routes/get-upload.ts @@ -0,0 +1,47 @@ +import { eq, getTableColumns } from 'drizzle-orm' +import { Elysia, t } from 'elysia' + +import { db } from '@/drizzle/client' +import { tag, tagToVideo, video } from '@/drizzle/schema' + +export const getUpload = new Elysia().get( + '/videos/:videoId', + async ({ params }) => { + const { videoId } = params + + const upload = await db.query.video.findFirst({ + with: { + tagToVideos: { + with: { + tag: { + columns: { + slug: true, + }, + }, + }, + }, + }, + where(fields, { eq }) { + return eq(fields.id, videoId) + }, + }) + + if (!upload) { + throw new Error('Video not found.') + } + + const { tagToVideos, ...video } = upload + + return { + video: { + ...video, + tags: tagToVideos.map((tagToVideo) => tagToVideo.tag), + }, + } + }, + { + params: t.Object({ + videoId: t.String(), + }), + }, +) diff --git a/server/routes/get-uploads.ts b/server/routes/get-uploads.ts index 122feb7..bd11858 100644 --- a/server/routes/get-uploads.ts +++ b/server/routes/get-uploads.ts @@ -1,54 +1,69 @@ -import { Prisma } from '@prisma/client' +import { + and, + count, + desc, + eq, + getTableColumns, + ilike, + inArray, +} from 'drizzle-orm' import { Elysia, t } from 'elysia' -import { prisma } from '@/lib/prisma' +import { db } from '@/drizzle/client' +import { tag, tagToVideo, transcription, video } from '@/drizzle/schema' export const getUploads = new Elysia().get( '/videos', async ({ query }) => { const { pageIndex, pageSize, titleFilter, tagsFilter } = query - const where: Prisma.VideoWhereInput = {} + const videoColumns = getTableColumns(video) - if (titleFilter) { - where.title = { - contains: titleFilter, - mode: 'insensitive', - } - } + const [videos, [{ amount }]] = await Promise.all([ + db + .select({ + ...videoColumns, + transcription: transcription.id, + }) + .from(video) + .leftJoin(tagToVideo, eq(tagToVideo.b, video.id)) + .leftJoin(tag, eq(tag.id, tagToVideo.a)) + .leftJoin(transcription, eq(transcription.videoId, video.id)) + .where( + and( + tagsFilter + ? inArray( + tag.slug, + Array.isArray(tagsFilter) ? tagsFilter : [tagsFilter], + ) + : undefined, + titleFilter ? ilike(video.title, `%${titleFilter}%`) : undefined, + ), + ) + .orderBy(desc(video.createdAt)) + .offset(pageIndex * pageSize) + .limit(pageSize) + .groupBy(video.id, transcription.id), - if (tagsFilter && tagsFilter.length > 0) { - where.tags = { - some: { - slug: { - in: Array.isArray(tagsFilter) ? tagsFilter : [tagsFilter], - }, - }, - } - } - - const [videos, count] = await Promise.all([ - prisma.video.findMany({ - include: { - transcription: { - select: { - id: true, - }, - }, - }, - where, - orderBy: { - createdAt: 'desc', - }, - skip: pageIndex * pageSize, - take: pageSize, - }), - prisma.video.count({ - where, - }), + db + .select({ amount: count() }) + .from(video) + .leftJoin(tagToVideo, eq(tagToVideo.b, video.id)) + .leftJoin(tag, eq(tag.id, tagToVideo.a)) + .where( + and( + tagsFilter + ? inArray( + tag.slug, + Array.isArray(tagsFilter) ? tagsFilter : [tagsFilter], + ) + : undefined, + titleFilter ? ilike(video.title, `%${titleFilter}%`) : undefined, + ), + ), ]) - const pageCount = Math.ceil(count / pageSize) + const pageCount = Math.ceil(amount / pageSize) return { videos, pageCount } }, diff --git a/server/routes/update-upload-transcription.ts b/server/routes/update-upload-transcription.ts deleted file mode 100644 index 5e26b67..0000000 --- a/server/routes/update-upload-transcription.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Elysia, t } from 'elysia' - -import { prisma } from '@/lib/prisma' - -export const updateUploadTranscription = new Elysia().put( - '/transcriptions', - async ({ body, set }) => { - const { segments } = body - - await prisma.$transaction( - segments.map((segment) => { - return prisma.transcriptionSegment.update({ - where: { - id: segment.id, - }, - data: { - text: segment.text, - }, - }) - }), - ) - - return new Response(null, { status: 204 }) - }, - { - body: t.Object({ - segments: t.Array( - t.Object({ - id: t.String(), - text: t.String(), - }), - { minItems: 1 }, - ), - }), - }, -) diff --git a/server/routes/update-upload.ts b/server/routes/update-upload.ts index 57fe705..70cfd17 100644 --- a/server/routes/update-upload.ts +++ b/server/routes/update-upload.ts @@ -1,7 +1,9 @@ +import { and, eq, inArray } from 'drizzle-orm' import { Elysia, t } from 'elysia' +import { db } from '@/drizzle/client' +import { tag, tagToVideo, video } from '@/drizzle/schema' import { publishMessagesOnTopic } from '@/lib/kafka' -import { prisma } from '@/lib/prisma' export const updateUpload = new Elysia().put( '/videos/:videoId', @@ -9,24 +11,75 @@ export const updateUpload = new Elysia().put( const { videoId } = params const { title, description, tags, commitUrl } = body - const { duration, externalProviderId } = await prisma.video.update({ - where: { - id: videoId, - }, - data: { - title, - description, - commitUrl, - tags: { - connect: tags.map((tag) => { - return { - slug: tag, - } - }), - }, - }, + const currentVideoTags = await db + .select({ id: tag.id, slug: tag.slug }) + .from(tag) + .innerJoin(tagToVideo, eq(tagToVideo.a, tag.id)) + .innerJoin(video, eq(tagToVideo.b, video.id)) + .where(eq(video.id, videoId)) + + const currentVideoTagsSlugs = currentVideoTags.map((item) => item.slug) + + const tagsToRemoveIds = currentVideoTags + .filter((item) => !tags.includes(item.slug)) + .map((item) => item.id) + + const tagsSlugsToAdd = tags.filter((slug) => { + return !currentVideoTagsSlugs.includes(slug) }) + const { duration, externalProviderId } = await db.transaction( + async (tx) => { + const [{ duration, externalProviderId }] = await tx + .update(video) + .set({ + title, + description, + commitUrl, + }) + .where(eq(video.id, videoId)) + .returning({ + duration: video.duration, + externalProviderId: video.externalProviderId, + }) + + if (tagsToRemoveIds.length > 0) { + await tx + .delete(tagToVideo) + .where( + and( + eq(tagToVideo.b, videoId), + inArray(tagToVideo.a, tagsToRemoveIds), + ), + ) + } + + if (tagsSlugsToAdd.length > 0) { + const tagsToAdd = await tx.query.tag.findMany({ + columns: { + id: true, + }, + where(fields, { inArray }) { + return inArray(fields.slug, tagsSlugsToAdd) + }, + }) + + const tagsToAddIds = tagsToAdd.map((item) => item.id) + + await tx.insert(tagToVideo).values( + tagsToAddIds.map((tagId) => { + return { + a: tagId, + b: videoId, + } + }), + ) + } + + return { duration, externalProviderId } + }, + ) + await publishMessagesOnTopic({ topic: 'jupiter.video-updated', messages: [ diff --git a/src/app/(app)/videos/[id]/page.tsx b/src/app/(app)/videos/[id]/page.tsx index 2c631ae..a3adadc 100644 --- a/src/app/(app)/videos/[id]/page.tsx +++ b/src/app/(app)/videos/[id]/page.tsx @@ -4,7 +4,6 @@ import { Metadata } from 'next' import { Button } from '@/components/ui/button' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' -import { prisma } from '@/lib/prisma' import { Overview } from './tabs/overview' import { Webhooks } from './tabs/webhooks' @@ -13,18 +12,9 @@ interface VideoPageProps { params: { id: string } } -export async function generateMetadata({ - params, -}: VideoPageProps): Promise { - const videoId = params.id - - const { title } = await prisma.video.findFirstOrThrow({ - where: { id: videoId }, - select: { title: true }, - }) - +export async function generateMetadata(): Promise { return { - title: `${title}`, + title: 'Video', } } diff --git a/src/app/(app)/videos/[id]/tabs/overview/index.tsx b/src/app/(app)/videos/[id]/tabs/overview/index.tsx index 1c876ce..31e9b51 100644 --- a/src/app/(app)/videos/[id]/tabs/overview/index.tsx +++ b/src/app/(app)/videos/[id]/tabs/overview/index.tsx @@ -1,3 +1,6 @@ +import { unstable_noStore } from 'next/cache' +import { headers } from 'next/headers' + import { Card, CardContent, @@ -5,7 +8,7 @@ import { CardHeader, CardTitle, } from '@/components/ui/card' -import { prisma } from '@/lib/prisma' +import { api } from '@/lib/eden' import { TranscriptionCard } from '../../transcription-card' import { VideoForm } from './video-form' @@ -15,19 +18,20 @@ export interface OverviewProps { } export async function Overview({ videoId }: OverviewProps) { - const video = await prisma.video.findFirstOrThrow({ - where: { - id: videoId, - }, - include: { - tags: { - select: { - slug: true, - }, - }, + unstable_noStore() + + const { data, error } = await api.videos[videoId].get({ + $fetch: { + headers: Object.fromEntries(headers().entries()), }, }) + if (error) { + throw error + } + + const { video } = data + return (

diff --git a/src/auth/auth.config.ts b/src/auth/auth.config.ts index 75d3b6e..bca2374 100644 --- a/src/auth/auth.config.ts +++ b/src/auth/auth.config.ts @@ -5,10 +5,8 @@ import { env } from '@/env' import { credentialsProvider } from './credentials-provider' import { googleProvider } from './google-provider' -import { prismaAdapter } from './prisma-auth-adapter' export const authConfig = { - adapter: prismaAdapter, providers: env.VERCEL_ENV === 'preview' ? [credentialsProvider] : [googleProvider], pages: { diff --git a/src/auth/prisma-auth-adapter.ts b/src/auth/prisma-auth-adapter.ts deleted file mode 100644 index 9061321..0000000 --- a/src/auth/prisma-auth-adapter.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { Adapter, AdapterUser } from '@auth/core/adapters' -import { PrismaClient } from '@prisma/client/edge' - -const prisma = new PrismaClient() - -export const prismaAdapter: Adapter = { - async createUser(userToCreate): Promise { - const { name, email, image } = userToCreate - const [, domain] = email.split('@') - - const company = await prisma.company.findUniqueOrThrow({ - where: { - domain, - }, - }) - - return await prisma.user.create({ - data: { - name, - email, - image, - companyId: company.id, - emailVerified: new Date(), - }, - }) - }, - - async getUser(id) { - const user = await prisma.user.findUnique({ - where: { id }, - }) - - if (!user) { - return null - } - - return user - }, - - async getUserByEmail(email) { - const user = await prisma.user.findUnique({ - where: { email }, - }) - - if (!user) { - return null - } - - return user - }, - - async getUserByAccount({ providerAccountId, provider }) { - const account = await prisma.account.findUnique({ - where: { - provider_providerAccountId: { - provider, - providerAccountId, - }, - }, - include: { - user: true, - }, - }) - - if (!account) { - return null - } - - const { user } = account - - return user - }, - - async updateUser(userToUpdate) { - const { id, name, email, image } = userToUpdate - - const user = await prisma.user.update({ - where: { id }, - data: { - name, - email, - image, - }, - }) - - return user - }, - - async linkAccount(account) { - await prisma.account.create({ - data: { - userId: account.userId, - type: account.type, - provider: account.provider, - providerAccountId: account.providerAccountId, - refreshToken: account.refresh_token, - accessToken: account.access_token, - expiresAt: account.expires_at, - tokenType: account.token_type, - scope: account.scope, - idToken: account.id_token, - sessionState: account.session_state?.toString(), - }, - }) - }, - - async createSession({ sessionToken, userId, expires }) { - const session = await prisma.session.create({ - data: { - userId, - expires, - sessionToken, - }, - }) - - return session - }, - - async getSessionAndUser(sessionToken) { - const prismaSession = await prisma.session.findUnique({ - where: { sessionToken }, - include: { - user: true, - }, - }) - - if (!prismaSession) { - return null - } - - const { user, ...session } = prismaSession - - return { user, session } - }, - - async updateSession({ sessionToken, userId, expires }) { - const session = await prisma.session.update({ - where: { sessionToken }, - data: { - expires, - userId, - }, - }) - - return session - }, - - async deleteSession(sessionToken) { - await prisma.session.delete({ - where: { sessionToken }, - }) - }, -} diff --git a/src/components/summary/storage.tsx b/src/components/summary/storage.tsx index 3ca23f8..7c5df56 100644 --- a/src/components/summary/storage.tsx +++ b/src/components/summary/storage.tsx @@ -1,30 +1,30 @@ import dayjs from 'dayjs' +import { gte, sum } from 'drizzle-orm' import { HardDrive } from 'lucide-react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { prisma } from '@/lib/prisma' +import { db } from '@/drizzle/client' +import { video } from '@/drizzle/schema' import { formatBytes } from '@/utils/format-bytes' export const revalidate = 60 * 15 // 15 minutes export async function Storage() { - const [total, lastMonth] = await Promise.all([ - prisma.video.aggregate({ - _sum: { - sizeInBytes: true, - }, - }), - prisma.video.aggregate({ - _sum: { - sizeInBytes: true, - }, - where: { - createdAt: { - gte: dayjs().subtract(30, 'days').toDate(), - }, - }, - }), - ]) + const [[{ sizeInBytesOverall }], [{ sizeInBytesLastMonth }]] = + await Promise.all([ + db + .select({ sizeInBytesOverall: sum(video.sizeInBytes).mapWith(Number) }) + .from(video), + + db + .select({ + sizeInBytesLastMonth: sum(video.sizeInBytes).mapWith(Number), + }) + .from(video) + .where( + gte(video.createdAt, dayjs().subtract(30, 'days').toISOString()), + ), + ]) return ( @@ -34,10 +34,10 @@ export async function Storage() { - {formatBytes(total._sum.sizeInBytes ?? 0)} + {formatBytes(sizeInBytesOverall ?? 0)}

- + {formatBytes(lastMonth._sum.sizeInBytes ?? 0)} in last 30 days + + {formatBytes(sizeInBytesLastMonth ?? 0)} in last 30 days

diff --git a/src/components/summary/total-count.tsx b/src/components/summary/total-count.tsx index cabc520..2c8bd24 100644 --- a/src/components/summary/total-count.tsx +++ b/src/components/summary/total-count.tsx @@ -1,29 +1,23 @@ import dayjs from 'dayjs' +import { count, gte } from 'drizzle-orm' import { BarChart } from 'lucide-react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { prisma } from '@/lib/prisma' +import { db } from '@/drizzle/client' +import { video } from '@/drizzle/schema' export const revalidate = 60 * 15 // 15 minutes export async function TotalCount() { - const [total, lastMonth] = await Promise.all([ - prisma.video.aggregate({ - _count: { - _all: true, - }, - }), + const [[{ amountOverall }], [{ amountLastMonth }]] = await Promise.all([ + db.select({ amountOverall: count().mapWith(Number) }).from(video), - prisma.video.aggregate({ - _count: { - _all: true, - }, - where: { - createdAt: { - gte: dayjs().subtract(30, 'days').toDate(), - }, - }, - }), + db + .select({ + amountLastMonth: count().mapWith(Number), + }) + .from(video) + .where(gte(video.createdAt, dayjs().subtract(30, 'days').toISOString())), ]) return ( @@ -34,10 +28,10 @@ export async function TotalCount() { - {String(total._count._all).padStart(4, '0')} + {String(amountOverall).padStart(4, '0')}

- + {lastMonth._count._all} in last 30 days + + {amountLastMonth} in last 30 days

diff --git a/src/drizzle/client.ts b/src/drizzle/client.ts new file mode 100644 index 0000000..e8d7c4e --- /dev/null +++ b/src/drizzle/client.ts @@ -0,0 +1,9 @@ +import { Pool } from '@neondatabase/serverless' +import { drizzle } from 'drizzle-orm/neon-serverless' + +import { env } from '../env' +import * as schema from './schema' + +const pool = new Pool({ connectionString: env.DATABASE_URL }) + +export const db = drizzle(pool, { schema }) diff --git a/src/drizzle/migrate.ts b/src/drizzle/migrate.ts new file mode 100644 index 0000000..58c8139 --- /dev/null +++ b/src/drizzle/migrate.ts @@ -0,0 +1,14 @@ +import { neon, neonConfig } from '@neondatabase/serverless' +import { drizzle } from 'drizzle-orm/neon-http' +import { migrate } from 'drizzle-orm/neon-http/migrator' + +import { env } from '../env' + +neonConfig.fetchConnectionCache = true + +const connection = neon(env.DATABASE_URL) +const db = drizzle(connection) + +migrate(db, { migrationsFolder: 'drizzle' }).then(() => { + console.log('Migrations applied successfully!') +}) diff --git a/src/drizzle/schema/account.ts b/src/drizzle/schema/account.ts new file mode 100644 index 0000000..50361df --- /dev/null +++ b/src/drizzle/schema/account.ts @@ -0,0 +1,38 @@ +import { relations } from 'drizzle-orm' +import { integer, pgTable, text, uniqueIndex, uuid } from 'drizzle-orm/pg-core' + +import { user } from '.' + +export const account = pgTable( + 'Account', + { + id: uuid('id').primaryKey().defaultRandom(), + userId: uuid('userId') + .notNull() + .references(() => user.id, { onDelete: 'cascade', onUpdate: 'cascade' }), + type: text('type').notNull(), + provider: text('provider').notNull(), + providerAccountId: text('providerAccountId').notNull(), + scope: text('scope'), + accessToken: text('accessToken'), + expiresAt: integer('expiresAt'), + idToken: text('idToken'), + refreshToken: text('refreshToken'), + sessionState: text('sessionState'), + tokenType: text('tokenType'), + }, + (table) => { + return { + providerProviderAccountIdKey: uniqueIndex( + 'Account_provider_providerAccountId_key', + ).on(table.provider, table.providerAccountId), + } + }, +) + +export const accountRelations = relations(account, ({ one }) => ({ + user: one(user, { + fields: [account.userId], + references: [user.id], + }), +})) diff --git a/src/drizzle/schema/company.ts b/src/drizzle/schema/company.ts new file mode 100644 index 0000000..83bb621 --- /dev/null +++ b/src/drizzle/schema/company.ts @@ -0,0 +1,24 @@ +import { relations } from 'drizzle-orm' +import { pgTable, text, uniqueIndex, uuid } from 'drizzle-orm/pg-core' + +import { tag, uploadBatch, video } from '.' + +export const company = pgTable( + 'Company', + { + id: uuid('id').primaryKey().defaultRandom(), + name: text('name').notNull(), + domain: text('domain').notNull(), + }, + (table) => { + return { + domainKey: uniqueIndex('Company_domain_key').on(table.domain), + } + }, +) + +export const companyRelations = relations(company, ({ many }) => ({ + videos: many(video), + uploadBatches: many(uploadBatch), + tags: many(tag), +})) diff --git a/src/drizzle/schema/index.ts b/src/drizzle/schema/index.ts new file mode 100644 index 0000000..453168a --- /dev/null +++ b/src/drizzle/schema/index.ts @@ -0,0 +1,12 @@ +export * from './account' +export * from './company' +export * from './session' +export * from './tag-to-video' +export * from './tag' +export * from './transcription' +export * from './transcription-segment' +export * from './upload-batch' +export * from './user' +export * from './verification-token' +export * from './video' +export * from './webhook' diff --git a/src/drizzle/schema/session.ts b/src/drizzle/schema/session.ts new file mode 100644 index 0000000..9a34029 --- /dev/null +++ b/src/drizzle/schema/session.ts @@ -0,0 +1,36 @@ +import { relations } from 'drizzle-orm' +import { + pgTable, + text, + timestamp, + uniqueIndex, + uuid, +} from 'drizzle-orm/pg-core' + +import { user } from '.' + +export const session = pgTable( + 'Session', + { + id: uuid('id').primaryKey().defaultRandom(), + sessionToken: text('sessionToken').notNull(), + userId: uuid('userId') + .notNull() + .references(() => user.id, { onDelete: 'cascade', onUpdate: 'cascade' }), + expires: timestamp('expires', { precision: 3, mode: 'string' }).notNull(), + }, + (table) => { + return { + sessionTokenKey: uniqueIndex('Session_sessionToken_key').on( + table.sessionToken, + ), + } + }, +) + +export const sessionRelations = relations(session, ({ one }) => ({ + user: one(user, { + fields: [session.userId], + references: [user.id], + }), +})) diff --git a/src/drizzle/schema/tag-to-video.ts b/src/drizzle/schema/tag-to-video.ts new file mode 100644 index 0000000..fff4c14 --- /dev/null +++ b/src/drizzle/schema/tag-to-video.ts @@ -0,0 +1,33 @@ +import { relations } from 'drizzle-orm' +import { index, pgTable, uniqueIndex, uuid } from 'drizzle-orm/pg-core' + +import { tag, video } from '.' + +export const tagToVideo = pgTable( + '_TagToVideo', + { + a: uuid('A') + .notNull() + .references(() => tag.id, { onDelete: 'cascade', onUpdate: 'cascade' }), + b: uuid('B') + .notNull() + .references(() => video.id, { onDelete: 'cascade', onUpdate: 'cascade' }), + }, + (table) => { + return { + abUnique: uniqueIndex('_TagToVideo_AB_unique').on(table.a, table.b), + bIdx: index().on(table.b), + } + }, +) + +export const tagToVideoRelations = relations(tagToVideo, ({ one }) => ({ + tag: one(tag, { + fields: [tagToVideo.a], + references: [tag.id], + }), + video: one(video, { + fields: [tagToVideo.b], + references: [video.id], + }), +})) diff --git a/src/drizzle/schema/tag.ts b/src/drizzle/schema/tag.ts new file mode 100644 index 0000000..1fba3e9 --- /dev/null +++ b/src/drizzle/schema/tag.ts @@ -0,0 +1,40 @@ +import { relations } from 'drizzle-orm' +import { + pgTable, + text, + timestamp, + uniqueIndex, + uuid, +} from 'drizzle-orm/pg-core' + +import { company, tagToVideo } from '.' + +export const tag = pgTable( + 'Tag', + { + id: uuid('id').primaryKey().defaultRandom(), + slug: text('slug').notNull(), + createdAt: timestamp('createdAt', { precision: 3, mode: 'string' }) + .defaultNow() + .notNull(), + companyId: uuid('companyId') + .notNull() + .references(() => company.id, { + onDelete: 'restrict', + onUpdate: 'cascade', + }), + }, + (table) => { + return { + slugKey: uniqueIndex('Tag_slug_key').on(table.slug), + } + }, +) + +export const tagRelations = relations(tag, ({ one, many }) => ({ + company: one(company, { + fields: [tag.companyId], + references: [company.id], + }), + tagToVideos: many(tagToVideo), +})) diff --git a/src/drizzle/schema/transcription-segment.ts b/src/drizzle/schema/transcription-segment.ts new file mode 100644 index 0000000..38f4f08 --- /dev/null +++ b/src/drizzle/schema/transcription-segment.ts @@ -0,0 +1,27 @@ +import { relations } from 'drizzle-orm' +import { numeric, pgTable, text, uuid } from 'drizzle-orm/pg-core' + +import { transcription } from '.' + +export const transcriptionSegment = pgTable('TranscriptionSegment', { + id: uuid('id').primaryKey().defaultRandom(), + transcriptionId: uuid('transcriptionId') + .notNull() + .references(() => transcription.id, { + onDelete: 'cascade', + onUpdate: 'cascade', + }), + start: numeric('start', { precision: 10, scale: 2 }).notNull(), + end: numeric('end', { precision: 10, scale: 2 }).notNull(), + text: text('text').notNull(), +}) + +export const transcriptionSegmentRelations = relations( + transcriptionSegment, + ({ one }) => ({ + transcription: one(transcription, { + fields: [transcriptionSegment.transcriptionId], + references: [transcription.id], + }), + }), +) diff --git a/src/drizzle/schema/transcription.ts b/src/drizzle/schema/transcription.ts new file mode 100644 index 0000000..e76fe42 --- /dev/null +++ b/src/drizzle/schema/transcription.ts @@ -0,0 +1,34 @@ +import { relations } from 'drizzle-orm' +import { pgTable, timestamp, uniqueIndex, uuid } from 'drizzle-orm/pg-core' + +import { transcriptionSegment, video } from '.' + +export const transcription = pgTable( + 'Transcription', + { + id: uuid('id').primaryKey().defaultRandom(), + videoId: uuid('videoId') + .notNull() + .references(() => video.id, { onDelete: 'cascade', onUpdate: 'cascade' }), + createdAt: timestamp('createdAt', { precision: 3, mode: 'string' }) + .defaultNow() + .notNull(), + reviewedAt: timestamp('reviewedAt', { precision: 3, mode: 'string' }), + }, + (table) => { + return { + videoIdKey: uniqueIndex('Transcription_videoId_key').on(table.videoId), + } + }, +) + +export const transcriptionRelations = relations( + transcription, + ({ one, many }) => ({ + video: one(video, { + fields: [transcription.videoId], + references: [video.id], + }), + segments: many(transcriptionSegment), + }), +) diff --git a/src/drizzle/schema/upload-batch.ts b/src/drizzle/schema/upload-batch.ts new file mode 100644 index 0000000..9dd7eb1 --- /dev/null +++ b/src/drizzle/schema/upload-batch.ts @@ -0,0 +1,25 @@ +import { relations } from 'drizzle-orm' +import { pgTable, timestamp, uuid } from 'drizzle-orm/pg-core' + +import { company, video } from '.' + +export const uploadBatch = pgTable('UploadBatch', { + id: uuid('id').primaryKey().defaultRandom(), + createdAt: timestamp('createdAt', { precision: 3, mode: 'string' }) + .defaultNow() + .notNull(), + companyId: uuid('companyId') + .notNull() + .references(() => company.id, { + onDelete: 'restrict', + onUpdate: 'cascade', + }), +}) + +export const uploadBatchRelations = relations(uploadBatch, ({ one, many }) => ({ + company: one(company, { + fields: [uploadBatch.companyId], + references: [company.id], + }), + videos: many(video), +})) diff --git a/src/drizzle/schema/user.ts b/src/drizzle/schema/user.ts new file mode 100644 index 0000000..80a0e5d --- /dev/null +++ b/src/drizzle/schema/user.ts @@ -0,0 +1,41 @@ +import { relations } from 'drizzle-orm' +import { + pgTable, + text, + timestamp, + uniqueIndex, + uuid, +} from 'drizzle-orm/pg-core' + +import { account, company, session } from '.' + +export const user = pgTable( + 'User', + { + id: uuid('id').primaryKey().defaultRandom(), + companyId: uuid('companyId') + .notNull() + .references(() => company.id, { + onDelete: 'restrict', + onUpdate: 'cascade', + }), + name: text('name'), + email: text('email').notNull(), + emailVerified: timestamp('emailVerified', { precision: 3, mode: 'string' }), + image: text('image'), + }, + (table) => { + return { + emailKey: uniqueIndex('User_email_key').on(table.email), + } + }, +) + +export const userRelations = relations(user, ({ one, many }) => ({ + company: one(company, { + fields: [user.companyId], + references: [company.id], + }), + sessions: many(session), + accounts: many(account), +})) diff --git a/src/drizzle/schema/verification-token.ts b/src/drizzle/schema/verification-token.ts new file mode 100644 index 0000000..f55f04c --- /dev/null +++ b/src/drizzle/schema/verification-token.ts @@ -0,0 +1,18 @@ +import { pgTable, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core' + +export const verificationToken = pgTable( + 'VerificationToken', + { + identifier: text('identifier').notNull(), + token: text('token').notNull(), + expires: timestamp('expires', { precision: 3, mode: 'string' }).notNull(), + }, + (table) => { + return { + tokenKey: uniqueIndex('VerificationToken_token_key').on(table.token), + identifierTokenKey: uniqueIndex( + 'VerificationToken_identifier_token_key', + ).on(table.identifier, table.token), + } + }, +) diff --git a/src/drizzle/schema/video.ts b/src/drizzle/schema/video.ts new file mode 100644 index 0000000..8476cb6 --- /dev/null +++ b/src/drizzle/schema/video.ts @@ -0,0 +1,67 @@ +import { relations } from 'drizzle-orm' +import { + integer, + pgTable, + text, + timestamp, + uniqueIndex, + uuid, +} from 'drizzle-orm/pg-core' + +import { company, tagToVideo, transcription, uploadBatch } from '.' + +export const video = pgTable( + 'Video', + { + id: uuid('id').primaryKey().defaultRandom(), + duration: integer('duration').notNull(), + title: text('title').notNull(), + storageKey: text('storageKey'), + description: text('description'), + uploadBatchId: uuid('uploadBatchId').references(() => uploadBatch.id, { + onDelete: 'set null', + onUpdate: 'cascade', + }), + companyId: uuid('companyId') + .notNull() + .references(() => company.id, { + onDelete: 'restrict', + onUpdate: 'cascade', + }), + externalProviderId: text('externalProviderId'), + audioStorageKey: text('audioStorageKey'), + processedAt: timestamp('processedAt', { precision: 3, mode: 'string' }), + sizeInBytes: integer('sizeInBytes').notNull(), + uploadOrder: integer('uploadOrder').notNull(), + commitUrl: text('commitUrl'), + subtitlesStorageKey: text('subtitlesStorageKey'), + language: text('language').default('pt').notNull(), + + createdAt: timestamp('createdAt', { precision: 3, mode: 'string' }) + .defaultNow() + .notNull(), + }, + (table) => { + return { + externalProviderIdKey: uniqueIndex('Video_externalProviderId_key').on( + table.externalProviderId, + ), + } + }, +) + +export const videoRelations = relations(video, ({ one, many }) => ({ + uploadBatch: one(uploadBatch, { + fields: [video.uploadBatchId], + references: [uploadBatch.id], + }), + company: one(company, { + fields: [video.companyId], + references: [company.id], + }), + transcription: one(transcription, { + fields: [video.id], + references: [transcription.videoId], + }), + tagToVideos: many(tagToVideo), +})) diff --git a/src/drizzle/schema/webhook.ts b/src/drizzle/schema/webhook.ts new file mode 100644 index 0000000..553cb77 --- /dev/null +++ b/src/drizzle/schema/webhook.ts @@ -0,0 +1,39 @@ +import { relations } from 'drizzle-orm' +import { pgEnum, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' + +import { video } from '.' + +export const webhookType = pgEnum('WebhookType', [ + 'CREATE_SUBTITLES_FROM_TRANSCRIPTION', + 'UPDATE_EXTERNAL_PROVIDER_STATUS', + 'UPLOAD_TO_EXTERNAL_PROVIDER', + 'CREATE_TRANSCRIPTION', + 'PROCESS_VIDEO', +]) + +export const webhookStatus = pgEnum('WebhookStatus', [ + 'ERROR', + 'SUCCESS', + 'RUNNING', +]) + +export const webhook = pgTable('Webhook', { + id: uuid('id').primaryKey().defaultRandom(), + videoId: uuid('videoId') + .notNull() + .references(() => video.id, { onDelete: 'cascade', onUpdate: 'cascade' }), + type: webhookType('type').notNull(), + status: webhookStatus('status').default('RUNNING').notNull(), + createdAt: timestamp('createdAt', { precision: 3, mode: 'string' }) + .defaultNow() + .notNull(), + finishedAt: timestamp('finishedAt', { precision: 3, mode: 'string' }), + metadata: text('metadata'), +}) + +export const webhookRelations = relations(webhook, ({ one }) => ({ + video: one(video, { + fields: [webhook.videoId], + references: [video.id], + }), +})) diff --git a/src/env.ts b/src/env.ts index d0fd607..bf71a36 100644 --- a/src/env.ts +++ b/src/env.ts @@ -1,20 +1,9 @@ import { createEnv } from '@t3-oss/env-nextjs' import { z } from 'zod' -const nodeEnv = z.enum(['development', 'production', 'test']) - -function requiredOnEnv(env: z.infer) { - return (value: any) => { - if (env === process.env.NODE_ENV && !value) { - return false - } - - return true - } -} - export const env = createEnv({ server: { + NODE_ENV: z.enum(['development', 'production', 'test']), DATABASE_URL: z.string().min(1), CLOUDFLARE_ACCOUNT_ID: z.string().min(1), CLOUDFLARE_ACCESS_KEY: z.string().min(1), @@ -23,35 +12,33 @@ export const env = createEnv({ CLOUDFLARE_UPLOAD_BUCKET_NAME: z.string().min(1), CLOUDFLARE_STORAGE_BUCKET_NAME: z.string().min(1), OPENAI_API_KEY: z.string().min(1), - PANDAVIDEO_API_KEY: z.string().refine(requiredOnEnv('production')), + PANDAVIDEO_API_KEY: z.string(), PANDAVIDEO_UPLOAD_FOLDER: z.string().uuid().min(1), NEXTAUTH_URL: z.string().optional(), AUTH_SECRET: z.string().min(1), GOOGLE_CLIENT_ID: z.string().min(1), GOOGLE_CLIENT_SECRET: z.string().min(1), - QSTASH_TOKEN: z.string().refine(requiredOnEnv('production')), - QSTASH_CURRENT_SIGNING_KEY: z.string().refine(requiredOnEnv('production')), - QSTASH_NEXT_SIGNING_KEY: z.string().refine(requiredOnEnv('production')), + QSTASH_TOKEN: z.string(), + QSTASH_CURRENT_SIGNING_KEY: z.string(), + QSTASH_NEXT_SIGNING_KEY: z.string(), QSTASH_VALIDATE_SIGNATURE: z .string() .transform((value) => value === 'true') .default('true'), - KAFKA_BROKER_URL: z.string().refine(requiredOnEnv('production')), - KAFKA_USERNAME: z.string().refine(requiredOnEnv('production')), - KAFKA_PASSWORD: z.string().refine(requiredOnEnv('production')), + KAFKA_BROKER_URL: z.string(), + KAFKA_USERNAME: z.string(), + KAFKA_PASSWORD: z.string(), }, client: { NEXT_PUBLIC_VERCEL_URL: z.string().url().min(1), }, shared: { - NODE_ENV: nodeEnv, VERCEL_ENV: z .enum(['production', 'preview', 'development']) .default('development'), }, experimental__runtimeEnv: { NEXT_PUBLIC_VERCEL_URL: process.env.NEXT_PUBLIC_VERCEL_URL, - NODE_ENV: process.env.NODE_ENV, VERCEL_ENV: process.env.VERCEL_ENV, }, }) diff --git a/src/middleware.ts b/src/middleware.ts index e7654a8..aae6753 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -5,5 +5,6 @@ import { authConfig } from '@/auth/auth.config' export default NextAuth(authConfig).auth export const config = { - matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'], + // matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'], + matcher: ['/none'], } diff --git a/tsconfig.json b/tsconfig.json index 07ccd09..c0305d8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "ESNext", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, From 95a5479405897ca3e847149e42621f32767f8e30 Mon Sep 17 00:00:00 2001 From: Diego Fernandes Date: Mon, 5 Feb 2024 10:45:27 -0300 Subject: [PATCH 04/21] feat: company structure --- drizzle/0002_simple_whizzer.sql | 11 + drizzle/0003_large_pestilence.sql | 11 + drizzle/0004_pale_lifeguard.sql | 8 + drizzle/meta/0002_snapshot.json | 840 +++++++++++++++++++++ drizzle/meta/0003_snapshot.json | 840 +++++++++++++++++++++ drizzle/meta/0004_snapshot.json | 824 ++++++++++++++++++++ drizzle/meta/_journal.json | 21 + server/routes/authentication.ts | 30 + server/routes/create-tag.ts | 9 +- server/routes/create-upload-batch.ts | 11 +- server/routes/errors/unauthorized-error.ts | 1 + server/routes/get-tags.ts | 18 +- server/routes/get-upload-webhooks.ts | 23 +- server/routes/get-upload.ts | 2 - server/routes/get-uploads.ts | 8 +- server/routes/update-upload.ts | 18 +- src/@types/node-auth-adapters.d.ts | 7 - src/@types/node-auth-extend.d.ts | 25 + src/auth/auth.config.ts | 33 +- src/auth/drizzle-auth-adapter.ts | 129 ++++ src/components/create-new-tag-dialog.tsx | 2 +- src/components/summary/storage.tsx | 4 +- src/components/summary/total-count.tsx | 2 +- src/drizzle/schema/account.ts | 11 +- src/drizzle/schema/session.ts | 33 +- src/drizzle/schema/tag.ts | 4 +- src/drizzle/schema/transcription.ts | 4 +- src/drizzle/schema/upload-batch.ts | 4 +- src/drizzle/schema/user.ts | 2 +- src/drizzle/schema/verification-token.ts | 16 +- src/drizzle/schema/video.ts | 6 +- src/drizzle/schema/webhook.ts | 6 +- src/middleware.ts | 6 +- 33 files changed, 2853 insertions(+), 116 deletions(-) create mode 100644 drizzle/0002_simple_whizzer.sql create mode 100644 drizzle/0003_large_pestilence.sql create mode 100644 drizzle/0004_pale_lifeguard.sql create mode 100644 drizzle/meta/0002_snapshot.json create mode 100644 drizzle/meta/0003_snapshot.json create mode 100644 drizzle/meta/0004_snapshot.json create mode 100644 server/routes/authentication.ts create mode 100644 server/routes/errors/unauthorized-error.ts delete mode 100644 src/@types/node-auth-adapters.d.ts create mode 100644 src/@types/node-auth-extend.d.ts create mode 100644 src/auth/drizzle-auth-adapter.ts diff --git a/drizzle/0002_simple_whizzer.sql b/drizzle/0002_simple_whizzer.sql new file mode 100644 index 0000000..bbb43d5 --- /dev/null +++ b/drizzle/0002_simple_whizzer.sql @@ -0,0 +1,11 @@ +ALTER TABLE "Session" ALTER COLUMN "expires" SET DATA TYPE timestamp (3);--> statement-breakpoint +ALTER TABLE "Tag" ALTER COLUMN "createdAt" SET DATA TYPE timestamp (3);--> statement-breakpoint +ALTER TABLE "Transcription" ALTER COLUMN "createdAt" SET DATA TYPE timestamp (3);--> statement-breakpoint +ALTER TABLE "Transcription" ALTER COLUMN "reviewedAt" SET DATA TYPE timestamp (3);--> statement-breakpoint +ALTER TABLE "UploadBatch" ALTER COLUMN "createdAt" SET DATA TYPE timestamp (3);--> statement-breakpoint +ALTER TABLE "User" ALTER COLUMN "emailVerified" SET DATA TYPE timestamp (3);--> statement-breakpoint +ALTER TABLE "VerificationToken" ALTER COLUMN "expires" SET DATA TYPE timestamp (3);--> statement-breakpoint +ALTER TABLE "Video" ALTER COLUMN "processedAt" SET DATA TYPE timestamp (3);--> statement-breakpoint +ALTER TABLE "Video" ALTER COLUMN "createdAt" SET DATA TYPE timestamp (3);--> statement-breakpoint +ALTER TABLE "Webhook" ALTER COLUMN "createdAt" SET DATA TYPE timestamp (3);--> statement-breakpoint +ALTER TABLE "Webhook" ALTER COLUMN "finishedAt" SET DATA TYPE timestamp (3); \ No newline at end of file diff --git a/drizzle/0003_large_pestilence.sql b/drizzle/0003_large_pestilence.sql new file mode 100644 index 0000000..aafeafa --- /dev/null +++ b/drizzle/0003_large_pestilence.sql @@ -0,0 +1,11 @@ +ALTER TABLE "Session" ALTER COLUMN "expires" SET DATA TYPE timestamp;--> statement-breakpoint +ALTER TABLE "Tag" ALTER COLUMN "createdAt" SET DATA TYPE timestamp;--> statement-breakpoint +ALTER TABLE "Transcription" ALTER COLUMN "createdAt" SET DATA TYPE timestamp;--> statement-breakpoint +ALTER TABLE "Transcription" ALTER COLUMN "reviewedAt" SET DATA TYPE timestamp;--> statement-breakpoint +ALTER TABLE "UploadBatch" ALTER COLUMN "createdAt" SET DATA TYPE timestamp;--> statement-breakpoint +ALTER TABLE "User" ALTER COLUMN "emailVerified" SET DATA TYPE timestamp;--> statement-breakpoint +ALTER TABLE "VerificationToken" ALTER COLUMN "expires" SET DATA TYPE timestamp;--> statement-breakpoint +ALTER TABLE "Video" ALTER COLUMN "processedAt" SET DATA TYPE timestamp;--> statement-breakpoint +ALTER TABLE "Video" ALTER COLUMN "createdAt" SET DATA TYPE timestamp;--> statement-breakpoint +ALTER TABLE "Webhook" ALTER COLUMN "createdAt" SET DATA TYPE timestamp;--> statement-breakpoint +ALTER TABLE "Webhook" ALTER COLUMN "finishedAt" SET DATA TYPE timestamp; \ No newline at end of file diff --git a/drizzle/0004_pale_lifeguard.sql b/drizzle/0004_pale_lifeguard.sql new file mode 100644 index 0000000..275de6c --- /dev/null +++ b/drizzle/0004_pale_lifeguard.sql @@ -0,0 +1,8 @@ +DROP INDEX IF EXISTS "Account_provider_providerAccountId_key";--> statement-breakpoint +DROP INDEX IF EXISTS "Session_sessionToken_key";--> statement-breakpoint +DROP INDEX IF EXISTS "VerificationToken_identifier_token_key";--> statement-breakpoint +ALTER TABLE "Session" ADD PRIMARY KEY ("sessionToken");--> statement-breakpoint +ALTER TABLE "Session" ALTER COLUMN "userId" SET DATA TYPE text;--> statement-breakpoint +ALTER TABLE "Account" ADD CONSTRAINT "Account_provider_providerAccountId_pk" PRIMARY KEY("provider","providerAccountId");--> statement-breakpoint +ALTER TABLE "VerificationToken" ADD CONSTRAINT "VerificationToken_identifier_token_pk" PRIMARY KEY("identifier","token");--> statement-breakpoint +ALTER TABLE "Session" DROP COLUMN IF EXISTS "id"; \ No newline at end of file diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..fc2d93f --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,840 @@ +{ + "id": "884419fc-f465-4f4b-beea-473a1687cd6d", + "prevId": "4f813968-c2e4-4c6e-a596-a5bcddf83665", + "version": "5", + "dialect": "pg", + "tables": { + "Account": { + "name": "Account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "idToken": { + "name": "idToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sessionState": { + "name": "sessionState", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokenType": { + "name": "tokenType", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Account_provider_providerAccountId_key": { + "name": "Account_provider_providerAccountId_key", + "columns": [ + "provider", + "providerAccountId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Account_userId_User_id_fk": { + "name": "Account_userId_User_id_fk", + "tableFrom": "Account", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Company": { + "name": "Company", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "Company_domain_key": { + "name": "Company_domain_key", + "columns": [ + "domain" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Session": { + "name": "Session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "Session_sessionToken_key": { + "name": "Session_sessionToken_key", + "columns": [ + "sessionToken" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Session_userId_User_id_fk": { + "name": "Session_userId_User_id_fk", + "tableFrom": "Session", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "_TagToVideo": { + "name": "_TagToVideo", + "schema": "", + "columns": { + "A": { + "name": "A", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "B": { + "name": "B", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "_TagToVideo_AB_unique": { + "name": "_TagToVideo_AB_unique", + "columns": [ + "A", + "B" + ], + "isUnique": true + }, + "_TagToVideo_B_index": { + "name": "_TagToVideo_B_index", + "columns": [ + "B" + ], + "isUnique": false + } + }, + "foreignKeys": { + "_TagToVideo_A_Tag_id_fk": { + "name": "_TagToVideo_A_Tag_id_fk", + "tableFrom": "_TagToVideo", + "tableTo": "Tag", + "columnsFrom": [ + "A" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "_TagToVideo_B_Video_id_fk": { + "name": "_TagToVideo_B_Video_id_fk", + "tableFrom": "_TagToVideo", + "tableTo": "Video", + "columnsFrom": [ + "B" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Tag": { + "name": "Tag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "companyId": { + "name": "companyId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "Tag_slug_key": { + "name": "Tag_slug_key", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Tag_companyId_Company_id_fk": { + "name": "Tag_companyId_Company_id_fk", + "tableFrom": "Tag", + "tableTo": "Company", + "columnsFrom": [ + "companyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Transcription": { + "name": "Transcription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "videoId": { + "name": "videoId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "reviewedAt": { + "name": "reviewedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Transcription_videoId_key": { + "name": "Transcription_videoId_key", + "columns": [ + "videoId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Transcription_videoId_Video_id_fk": { + "name": "Transcription_videoId_Video_id_fk", + "tableFrom": "Transcription", + "tableTo": "Video", + "columnsFrom": [ + "videoId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "TranscriptionSegment": { + "name": "TranscriptionSegment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "transcriptionId": { + "name": "transcriptionId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "start": { + "name": "start", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "end": { + "name": "end", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "TranscriptionSegment_transcriptionId_Transcription_id_fk": { + "name": "TranscriptionSegment_transcriptionId_Transcription_id_fk", + "tableFrom": "TranscriptionSegment", + "tableTo": "Transcription", + "columnsFrom": [ + "transcriptionId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "UploadBatch": { + "name": "UploadBatch", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "companyId": { + "name": "companyId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "UploadBatch_companyId_Company_id_fk": { + "name": "UploadBatch_companyId_Company_id_fk", + "tableFrom": "UploadBatch", + "tableTo": "Company", + "columnsFrom": [ + "companyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "User": { + "name": "User", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "companyId": { + "name": "companyId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "User_email_key": { + "name": "User_email_key", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": { + "User_companyId_Company_id_fk": { + "name": "User_companyId_Company_id_fk", + "tableFrom": "User", + "tableTo": "Company", + "columnsFrom": [ + "companyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "VerificationToken": { + "name": "VerificationToken", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "VerificationToken_token_key": { + "name": "VerificationToken_token_key", + "columns": [ + "token" + ], + "isUnique": true + }, + "VerificationToken_identifier_token_key": { + "name": "VerificationToken_identifier_token_key", + "columns": [ + "identifier", + "token" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Video": { + "name": "Video", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storageKey": { + "name": "storageKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploadBatchId": { + "name": "uploadBatchId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "companyId": { + "name": "companyId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "externalProviderId": { + "name": "externalProviderId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audioStorageKey": { + "name": "audioStorageKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processedAt": { + "name": "processedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "sizeInBytes": { + "name": "sizeInBytes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "uploadOrder": { + "name": "uploadOrder", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "commitUrl": { + "name": "commitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subtitlesStorageKey": { + "name": "subtitlesStorageKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pt'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "Video_externalProviderId_key": { + "name": "Video_externalProviderId_key", + "columns": [ + "externalProviderId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Video_uploadBatchId_UploadBatch_id_fk": { + "name": "Video_uploadBatchId_UploadBatch_id_fk", + "tableFrom": "Video", + "tableTo": "UploadBatch", + "columnsFrom": [ + "uploadBatchId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Video_companyId_Company_id_fk": { + "name": "Video_companyId_Company_id_fk", + "tableFrom": "Video", + "tableTo": "Company", + "columnsFrom": [ + "companyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Webhook": { + "name": "Webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "videoId": { + "name": "videoId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "WebhookType", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "WebhookStatus", + "primaryKey": false, + "notNull": true, + "default": "'RUNNING'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finishedAt": { + "name": "finishedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Webhook_videoId_Video_id_fk": { + "name": "Webhook_videoId_Video_id_fk", + "tableFrom": "Webhook", + "tableTo": "Video", + "columnsFrom": [ + "videoId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": { + "WebhookStatus": { + "name": "WebhookStatus", + "values": { + "ERROR": "ERROR", + "SUCCESS": "SUCCESS", + "RUNNING": "RUNNING" + } + }, + "WebhookType": { + "name": "WebhookType", + "values": { + "CREATE_SUBTITLES_FROM_TRANSCRIPTION": "CREATE_SUBTITLES_FROM_TRANSCRIPTION", + "UPDATE_EXTERNAL_PROVIDER_STATUS": "UPDATE_EXTERNAL_PROVIDER_STATUS", + "UPLOAD_TO_EXTERNAL_PROVIDER": "UPLOAD_TO_EXTERNAL_PROVIDER", + "CREATE_TRANSCRIPTION": "CREATE_TRANSCRIPTION", + "PROCESS_VIDEO": "PROCESS_VIDEO" + } + } + }, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0003_snapshot.json b/drizzle/meta/0003_snapshot.json new file mode 100644 index 0000000..45591d1 --- /dev/null +++ b/drizzle/meta/0003_snapshot.json @@ -0,0 +1,840 @@ +{ + "id": "3e158fe9-7da4-46e9-a7f4-69380d68e7b6", + "prevId": "884419fc-f465-4f4b-beea-473a1687cd6d", + "version": "5", + "dialect": "pg", + "tables": { + "Account": { + "name": "Account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "idToken": { + "name": "idToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sessionState": { + "name": "sessionState", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokenType": { + "name": "tokenType", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Account_provider_providerAccountId_key": { + "name": "Account_provider_providerAccountId_key", + "columns": [ + "provider", + "providerAccountId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Account_userId_User_id_fk": { + "name": "Account_userId_User_id_fk", + "tableFrom": "Account", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Company": { + "name": "Company", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "Company_domain_key": { + "name": "Company_domain_key", + "columns": [ + "domain" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Session": { + "name": "Session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "Session_sessionToken_key": { + "name": "Session_sessionToken_key", + "columns": [ + "sessionToken" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Session_userId_User_id_fk": { + "name": "Session_userId_User_id_fk", + "tableFrom": "Session", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "_TagToVideo": { + "name": "_TagToVideo", + "schema": "", + "columns": { + "A": { + "name": "A", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "B": { + "name": "B", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "_TagToVideo_AB_unique": { + "name": "_TagToVideo_AB_unique", + "columns": [ + "A", + "B" + ], + "isUnique": true + }, + "_TagToVideo_B_index": { + "name": "_TagToVideo_B_index", + "columns": [ + "B" + ], + "isUnique": false + } + }, + "foreignKeys": { + "_TagToVideo_A_Tag_id_fk": { + "name": "_TagToVideo_A_Tag_id_fk", + "tableFrom": "_TagToVideo", + "tableTo": "Tag", + "columnsFrom": [ + "A" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "_TagToVideo_B_Video_id_fk": { + "name": "_TagToVideo_B_Video_id_fk", + "tableFrom": "_TagToVideo", + "tableTo": "Video", + "columnsFrom": [ + "B" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Tag": { + "name": "Tag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "companyId": { + "name": "companyId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "Tag_slug_key": { + "name": "Tag_slug_key", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Tag_companyId_Company_id_fk": { + "name": "Tag_companyId_Company_id_fk", + "tableFrom": "Tag", + "tableTo": "Company", + "columnsFrom": [ + "companyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Transcription": { + "name": "Transcription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "videoId": { + "name": "videoId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "reviewedAt": { + "name": "reviewedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Transcription_videoId_key": { + "name": "Transcription_videoId_key", + "columns": [ + "videoId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Transcription_videoId_Video_id_fk": { + "name": "Transcription_videoId_Video_id_fk", + "tableFrom": "Transcription", + "tableTo": "Video", + "columnsFrom": [ + "videoId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "TranscriptionSegment": { + "name": "TranscriptionSegment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "transcriptionId": { + "name": "transcriptionId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "start": { + "name": "start", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "end": { + "name": "end", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "TranscriptionSegment_transcriptionId_Transcription_id_fk": { + "name": "TranscriptionSegment_transcriptionId_Transcription_id_fk", + "tableFrom": "TranscriptionSegment", + "tableTo": "Transcription", + "columnsFrom": [ + "transcriptionId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "UploadBatch": { + "name": "UploadBatch", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "companyId": { + "name": "companyId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "UploadBatch_companyId_Company_id_fk": { + "name": "UploadBatch_companyId_Company_id_fk", + "tableFrom": "UploadBatch", + "tableTo": "Company", + "columnsFrom": [ + "companyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "User": { + "name": "User", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "companyId": { + "name": "companyId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "User_email_key": { + "name": "User_email_key", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": { + "User_companyId_Company_id_fk": { + "name": "User_companyId_Company_id_fk", + "tableFrom": "User", + "tableTo": "Company", + "columnsFrom": [ + "companyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "VerificationToken": { + "name": "VerificationToken", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "VerificationToken_token_key": { + "name": "VerificationToken_token_key", + "columns": [ + "token" + ], + "isUnique": true + }, + "VerificationToken_identifier_token_key": { + "name": "VerificationToken_identifier_token_key", + "columns": [ + "identifier", + "token" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Video": { + "name": "Video", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storageKey": { + "name": "storageKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploadBatchId": { + "name": "uploadBatchId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "companyId": { + "name": "companyId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "externalProviderId": { + "name": "externalProviderId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audioStorageKey": { + "name": "audioStorageKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processedAt": { + "name": "processedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "sizeInBytes": { + "name": "sizeInBytes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "uploadOrder": { + "name": "uploadOrder", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "commitUrl": { + "name": "commitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subtitlesStorageKey": { + "name": "subtitlesStorageKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pt'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "Video_externalProviderId_key": { + "name": "Video_externalProviderId_key", + "columns": [ + "externalProviderId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Video_uploadBatchId_UploadBatch_id_fk": { + "name": "Video_uploadBatchId_UploadBatch_id_fk", + "tableFrom": "Video", + "tableTo": "UploadBatch", + "columnsFrom": [ + "uploadBatchId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Video_companyId_Company_id_fk": { + "name": "Video_companyId_Company_id_fk", + "tableFrom": "Video", + "tableTo": "Company", + "columnsFrom": [ + "companyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Webhook": { + "name": "Webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "videoId": { + "name": "videoId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "WebhookType", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "WebhookStatus", + "primaryKey": false, + "notNull": true, + "default": "'RUNNING'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finishedAt": { + "name": "finishedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Webhook_videoId_Video_id_fk": { + "name": "Webhook_videoId_Video_id_fk", + "tableFrom": "Webhook", + "tableTo": "Video", + "columnsFrom": [ + "videoId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": { + "WebhookStatus": { + "name": "WebhookStatus", + "values": { + "ERROR": "ERROR", + "SUCCESS": "SUCCESS", + "RUNNING": "RUNNING" + } + }, + "WebhookType": { + "name": "WebhookType", + "values": { + "CREATE_SUBTITLES_FROM_TRANSCRIPTION": "CREATE_SUBTITLES_FROM_TRANSCRIPTION", + "UPDATE_EXTERNAL_PROVIDER_STATUS": "UPDATE_EXTERNAL_PROVIDER_STATUS", + "UPLOAD_TO_EXTERNAL_PROVIDER": "UPLOAD_TO_EXTERNAL_PROVIDER", + "CREATE_TRANSCRIPTION": "CREATE_TRANSCRIPTION", + "PROCESS_VIDEO": "PROCESS_VIDEO" + } + } + }, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0004_snapshot.json b/drizzle/meta/0004_snapshot.json new file mode 100644 index 0000000..64b984d --- /dev/null +++ b/drizzle/meta/0004_snapshot.json @@ -0,0 +1,824 @@ +{ + "id": "54105f35-5973-4c0d-aea9-b9d5e983a18a", + "prevId": "3e158fe9-7da4-46e9-a7f4-69380d68e7b6", + "version": "5", + "dialect": "pg", + "tables": { + "Account": { + "name": "Account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "idToken": { + "name": "idToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sessionState": { + "name": "sessionState", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokenType": { + "name": "tokenType", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Account_userId_User_id_fk": { + "name": "Account_userId_User_id_fk", + "tableFrom": "Account", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "Account_provider_providerAccountId_pk": { + "name": "Account_provider_providerAccountId_pk", + "columns": [ + "provider", + "providerAccountId" + ] + } + }, + "uniqueConstraints": {} + }, + "Company": { + "name": "Company", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "Company_domain_key": { + "name": "Company_domain_key", + "columns": [ + "domain" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Session": { + "name": "Session", + "schema": "", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "Session_userId_User_id_fk": { + "name": "Session_userId_User_id_fk", + "tableFrom": "Session", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "_TagToVideo": { + "name": "_TagToVideo", + "schema": "", + "columns": { + "A": { + "name": "A", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "B": { + "name": "B", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "_TagToVideo_AB_unique": { + "name": "_TagToVideo_AB_unique", + "columns": [ + "A", + "B" + ], + "isUnique": true + }, + "_TagToVideo_B_index": { + "name": "_TagToVideo_B_index", + "columns": [ + "B" + ], + "isUnique": false + } + }, + "foreignKeys": { + "_TagToVideo_A_Tag_id_fk": { + "name": "_TagToVideo_A_Tag_id_fk", + "tableFrom": "_TagToVideo", + "tableTo": "Tag", + "columnsFrom": [ + "A" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "_TagToVideo_B_Video_id_fk": { + "name": "_TagToVideo_B_Video_id_fk", + "tableFrom": "_TagToVideo", + "tableTo": "Video", + "columnsFrom": [ + "B" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Tag": { + "name": "Tag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "companyId": { + "name": "companyId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "Tag_slug_key": { + "name": "Tag_slug_key", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Tag_companyId_Company_id_fk": { + "name": "Tag_companyId_Company_id_fk", + "tableFrom": "Tag", + "tableTo": "Company", + "columnsFrom": [ + "companyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Transcription": { + "name": "Transcription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "videoId": { + "name": "videoId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "reviewedAt": { + "name": "reviewedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Transcription_videoId_key": { + "name": "Transcription_videoId_key", + "columns": [ + "videoId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Transcription_videoId_Video_id_fk": { + "name": "Transcription_videoId_Video_id_fk", + "tableFrom": "Transcription", + "tableTo": "Video", + "columnsFrom": [ + "videoId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "TranscriptionSegment": { + "name": "TranscriptionSegment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "transcriptionId": { + "name": "transcriptionId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "start": { + "name": "start", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "end": { + "name": "end", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "TranscriptionSegment_transcriptionId_Transcription_id_fk": { + "name": "TranscriptionSegment_transcriptionId_Transcription_id_fk", + "tableFrom": "TranscriptionSegment", + "tableTo": "Transcription", + "columnsFrom": [ + "transcriptionId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "UploadBatch": { + "name": "UploadBatch", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "companyId": { + "name": "companyId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "UploadBatch_companyId_Company_id_fk": { + "name": "UploadBatch_companyId_Company_id_fk", + "tableFrom": "UploadBatch", + "tableTo": "Company", + "columnsFrom": [ + "companyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "User": { + "name": "User", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "companyId": { + "name": "companyId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "User_email_key": { + "name": "User_email_key", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": { + "User_companyId_Company_id_fk": { + "name": "User_companyId_Company_id_fk", + "tableFrom": "User", + "tableTo": "Company", + "columnsFrom": [ + "companyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "VerificationToken": { + "name": "VerificationToken", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "VerificationToken_token_key": { + "name": "VerificationToken_token_key", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "VerificationToken_identifier_token_pk": { + "name": "VerificationToken_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {} + }, + "Video": { + "name": "Video", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storageKey": { + "name": "storageKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploadBatchId": { + "name": "uploadBatchId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "companyId": { + "name": "companyId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "externalProviderId": { + "name": "externalProviderId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audioStorageKey": { + "name": "audioStorageKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processedAt": { + "name": "processedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "sizeInBytes": { + "name": "sizeInBytes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "uploadOrder": { + "name": "uploadOrder", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "commitUrl": { + "name": "commitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subtitlesStorageKey": { + "name": "subtitlesStorageKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pt'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "Video_externalProviderId_key": { + "name": "Video_externalProviderId_key", + "columns": [ + "externalProviderId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Video_uploadBatchId_UploadBatch_id_fk": { + "name": "Video_uploadBatchId_UploadBatch_id_fk", + "tableFrom": "Video", + "tableTo": "UploadBatch", + "columnsFrom": [ + "uploadBatchId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Video_companyId_Company_id_fk": { + "name": "Video_companyId_Company_id_fk", + "tableFrom": "Video", + "tableTo": "Company", + "columnsFrom": [ + "companyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Webhook": { + "name": "Webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "videoId": { + "name": "videoId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "WebhookType", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "WebhookStatus", + "primaryKey": false, + "notNull": true, + "default": "'RUNNING'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finishedAt": { + "name": "finishedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Webhook_videoId_Video_id_fk": { + "name": "Webhook_videoId_Video_id_fk", + "tableFrom": "Webhook", + "tableTo": "Video", + "columnsFrom": [ + "videoId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": { + "WebhookStatus": { + "name": "WebhookStatus", + "values": { + "ERROR": "ERROR", + "SUCCESS": "SUCCESS", + "RUNNING": "RUNNING" + } + }, + "WebhookType": { + "name": "WebhookType", + "values": { + "CREATE_SUBTITLES_FROM_TRANSCRIPTION": "CREATE_SUBTITLES_FROM_TRANSCRIPTION", + "UPDATE_EXTERNAL_PROVIDER_STATUS": "UPDATE_EXTERNAL_PROVIDER_STATUS", + "UPLOAD_TO_EXTERNAL_PROVIDER": "UPLOAD_TO_EXTERNAL_PROVIDER", + "CREATE_TRANSCRIPTION": "CREATE_TRANSCRIPTION", + "PROCESS_VIDEO": "PROCESS_VIDEO" + } + } + }, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 59b7302..adc3b99 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -15,6 +15,27 @@ "when": 1706875127486, "tag": "0001_massive_jack_murdock", "breakpoints": true + }, + { + "idx": 2, + "version": "5", + "when": 1706910071429, + "tag": "0002_simple_whizzer", + "breakpoints": true + }, + { + "idx": 3, + "version": "5", + "when": 1706911259532, + "tag": "0003_large_pestilence", + "breakpoints": true + }, + { + "idx": 4, + "version": "5", + "when": 1707135552158, + "tag": "0004_pale_lifeguard", + "breakpoints": true } ] } \ No newline at end of file diff --git a/server/routes/authentication.ts b/server/routes/authentication.ts new file mode 100644 index 0000000..1ace104 --- /dev/null +++ b/server/routes/authentication.ts @@ -0,0 +1,30 @@ +import { Elysia } from 'elysia' + +import { auth } from '@/auth' + +import { UnauthorizedError } from './errors/unauthorized-error' + +export const authentication = new Elysia() + .error({ + UNAUTHORIZED: UnauthorizedError, + }) + .onError(({ code, error, set }) => { + switch (code) { + case 'UNAUTHORIZED': + set.status = 401 + return { code, message: error.message } + } + }) + .derive(() => { + return { + getCurrentUser: async () => { + const session = await auth() + + if (!session) { + throw new UnauthorizedError() + } + + return session.user + }, + } + }) diff --git a/server/routes/create-tag.ts b/server/routes/create-tag.ts index da11cd7..b3358d3 100644 --- a/server/routes/create-tag.ts +++ b/server/routes/create-tag.ts @@ -3,14 +3,17 @@ import { Elysia, t } from 'elysia' import { db } from '@/drizzle/client' import { tag } from '@/drizzle/schema' -export const createTag = new Elysia().post( +import { authentication } from './authentication' + +export const createTag = new Elysia().use(authentication).post( '/tags', - async ({ body }) => { + async ({ body, getCurrentUser }) => { + const { companyId } = await getCurrentUser() const { tag: slug } = body await db.insert(tag).values({ slug, - companyId: 'ae6780ef-b2c2-4041-bada-c48e27ac6157', + companyId, }) return new Response(null, { status: 201 }) diff --git a/server/routes/create-upload-batch.ts b/server/routes/create-upload-batch.ts index f2046a1..6edb2fc 100644 --- a/server/routes/create-upload-batch.ts +++ b/server/routes/create-upload-batch.ts @@ -5,16 +5,19 @@ import { tagToVideo, uploadBatch, video } from '@/drizzle/schema' import { publishMessagesOnTopic } from '@/lib/kafka' import { publishMessage } from '@/lib/qstash' -export const createUploadBatch = new Elysia().post( +import { authentication } from './authentication' + +export const createUploadBatch = new Elysia().use(authentication).post( '/batches', - async ({ body, set }) => { + async ({ body, set, getCurrentUser }) => { + const { companyId } = await getCurrentUser() const { files: videos } = body const { batchId } = await db.transaction(async (tx) => { const [{ id: batchId }] = await tx .insert(uploadBatch) .values({ - companyId: 'ae6780ef-b2c2-4041-bada-c48e27ac6157', + companyId, }) .returning({ id: uploadBatch.id, @@ -30,7 +33,7 @@ export const createUploadBatch = new Elysia().post( title: videoItem.title, sizeInBytes: videoItem.sizeInBytes, duration: videoItem.duration, - companyId: 'ae6780ef-b2c2-4041-bada-c48e27ac6157', + companyId, } }), ) diff --git a/server/routes/errors/unauthorized-error.ts b/server/routes/errors/unauthorized-error.ts new file mode 100644 index 0000000..2e48207 --- /dev/null +++ b/server/routes/errors/unauthorized-error.ts @@ -0,0 +1 @@ +export class UnauthorizedError extends Error {} diff --git a/server/routes/get-tags.ts b/server/routes/get-tags.ts index aeeaf1e..42f467e 100644 --- a/server/routes/get-tags.ts +++ b/server/routes/get-tags.ts @@ -4,19 +4,21 @@ import { Elysia, t } from 'elysia' import { db } from '@/drizzle/client' import { tag } from '@/drizzle/schema' -export const getTags = new Elysia().get( +import { authentication } from './authentication' + +export const getTags = new Elysia().use(authentication).get( '/tags/search', - async ({ query }) => { + async ({ query, getCurrentUser }) => { + const { companyId } = await getCurrentUser() const { q: search, pageIndex, pageSize } = query const [tags, [{ amount }]] = await Promise.all([ db.query.tag.findMany({ - where(fields, { ilike }) { - if (search) { - return ilike(fields.slug, `%${search}%`) - } - - return undefined + where(fields, { ilike, eq, and }) { + return and( + eq(fields.companyId, companyId), + search ? ilike(fields.slug, `%${search}%`) : undefined, + ) }, offset: pageIndex * pageSize, limit: pageSize, diff --git a/server/routes/get-upload-webhooks.ts b/server/routes/get-upload-webhooks.ts index dc1dc41..1475141 100644 --- a/server/routes/get-upload-webhooks.ts +++ b/server/routes/get-upload-webhooks.ts @@ -1,20 +1,23 @@ +import { and, desc, eq, getTableColumns } from 'drizzle-orm' import { Elysia, t } from 'elysia' import { db } from '@/drizzle/client' +import { video, webhook } from '@/drizzle/schema' -export const getUploadWebhooks = new Elysia().get( +import { authentication } from './authentication' + +export const getUploadWebhooks = new Elysia().use(authentication).get( '/videos/:videoId/webhooks', - async ({ params }) => { + async ({ params, getCurrentUser }) => { + const { companyId } = await getCurrentUser() const { videoId } = params - const webhooks = await db.query.webhook.findMany({ - where(fields, { eq }) { - return eq(fields.videoId, videoId) - }, - orderBy(fields, { desc }) { - return desc(fields.createdAt) - }, - }) + const webhooks = await db + .select(getTableColumns(webhook)) + .from(webhook) + .innerJoin(video, eq(video.id, webhook.videoId)) + .where(and(eq(webhook.videoId, videoId), eq(video.companyId, companyId))) + .orderBy(desc(webhook.createdAt)) return { webhooks } }, diff --git a/server/routes/get-upload.ts b/server/routes/get-upload.ts index 7abd7bd..c7d6911 100644 --- a/server/routes/get-upload.ts +++ b/server/routes/get-upload.ts @@ -1,8 +1,6 @@ -import { eq, getTableColumns } from 'drizzle-orm' import { Elysia, t } from 'elysia' import { db } from '@/drizzle/client' -import { tag, tagToVideo, video } from '@/drizzle/schema' export const getUpload = new Elysia().get( '/videos/:videoId', diff --git a/server/routes/get-uploads.ts b/server/routes/get-uploads.ts index bd11858..cd9ad69 100644 --- a/server/routes/get-uploads.ts +++ b/server/routes/get-uploads.ts @@ -12,9 +12,12 @@ import { Elysia, t } from 'elysia' import { db } from '@/drizzle/client' import { tag, tagToVideo, transcription, video } from '@/drizzle/schema' -export const getUploads = new Elysia().get( +import { authentication } from './authentication' + +export const getUploads = new Elysia().use(authentication).get( '/videos', - async ({ query }) => { + async ({ query, getCurrentUser }) => { + const { companyId } = await getCurrentUser() const { pageIndex, pageSize, titleFilter, tagsFilter } = query const videoColumns = getTableColumns(video) @@ -31,6 +34,7 @@ export const getUploads = new Elysia().get( .leftJoin(transcription, eq(transcription.videoId, video.id)) .where( and( + eq(video.companyId, companyId), tagsFilter ? inArray( tag.slug, diff --git a/server/routes/update-upload.ts b/server/routes/update-upload.ts index 70cfd17..c618881 100644 --- a/server/routes/update-upload.ts +++ b/server/routes/update-upload.ts @@ -5,12 +5,26 @@ import { db } from '@/drizzle/client' import { tag, tagToVideo, video } from '@/drizzle/schema' import { publishMessagesOnTopic } from '@/lib/kafka' -export const updateUpload = new Elysia().put( +import { authentication } from './authentication' +import { UnauthorizedError } from './errors/unauthorized-error' + +export const updateUpload = new Elysia().use(authentication).put( '/videos/:videoId', - async ({ params, body }) => { + async ({ params, body, getCurrentUser }) => { + const { companyId } = await getCurrentUser() const { videoId } = params const { title, description, tags, commitUrl } = body + const videoFromUser = await db.query.video.findFirst({ + where(fields, { eq, and }) { + return and(eq(fields.id, videoId), eq(fields.companyId, companyId)) + }, + }) + + if (!videoFromUser) { + throw new UnauthorizedError() + } + const currentVideoTags = await db .select({ id: tag.id, slug: tag.slug }) .from(tag) diff --git a/src/@types/node-auth-adapters.d.ts b/src/@types/node-auth-adapters.d.ts deleted file mode 100644 index a31f488..0000000 --- a/src/@types/node-auth-adapters.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { AdapterUser as AdapterUserBase } from '@auth/core/adapters' - -declare module '@auth/core/adapters' { - interface AdapterUser extends AdapterUserBase { - companyId: string - } -} diff --git a/src/@types/node-auth-extend.d.ts b/src/@types/node-auth-extend.d.ts new file mode 100644 index 0000000..f0f2e67 --- /dev/null +++ b/src/@types/node-auth-extend.d.ts @@ -0,0 +1,25 @@ +import { AdapterUser as AdapterUserBase } from '@auth/core/adapters' +import { DefaultSession, User as DefaultUser } from 'next-auth' +import { DefaultJWT } from 'next-auth/jwt' + +declare module '@auth/core/adapters' { + interface AdapterUser extends AdapterUserBase { + companyId: string + } +} + +declare module 'next-auth' { + interface User extends DefaultUser { + companyId: string + } + + interface Session extends DefaultSession { + user: User + } +} + +declare module 'next-auth/jwt' { + interface JWT extends DefaultJWT { + companyId: string + } +} diff --git a/src/auth/auth.config.ts b/src/auth/auth.config.ts index bca2374..264d46a 100644 --- a/src/auth/auth.config.ts +++ b/src/auth/auth.config.ts @@ -1,39 +1,36 @@ import type { NextAuthConfig } from 'next-auth' -import { GoogleProfile } from 'next-auth/providers/google' import { env } from '@/env' import { credentialsProvider } from './credentials-provider' +import { drizzleAuthAdapter } from './drizzle-auth-adapter' import { googleProvider } from './google-provider' export const authConfig = { + adapter: drizzleAuthAdapter, providers: env.VERCEL_ENV === 'preview' ? [credentialsProvider] : [googleProvider], pages: { signIn: '/auth/sign-in', error: '/auth/error', }, + session: { + strategy: 'jwt', + }, callbacks: { - async signIn({ account, profile }) { - if (account?.provider === 'google') { - const googleProfile = profile as GoogleProfile - const emailDomain = googleProfile.email.match(/@([^@]+)$/) - - console.log(emailDomain) - - if (!emailDomain) { - return false - } + jwt({ token, user }) { + if (user) { + token.companyId = user.companyId + } - return ( - googleProfile.email_verified && - ['@rocketseat.team', '@digitalhouse.com'].includes(emailDomain[0]) - ) - } else if (account?.provider === 'credentials') { - return true + return token + }, + session({ session, ...params }) { + if ('token' in params && session.user) { + session.user.companyId = params.token.companyId } - return false + return session }, authorized({ auth, request: { nextUrl } }) { const isLoggedIn = !!auth?.user diff --git a/src/auth/drizzle-auth-adapter.ts b/src/auth/drizzle-auth-adapter.ts new file mode 100644 index 0000000..7c7f7b8 --- /dev/null +++ b/src/auth/drizzle-auth-adapter.ts @@ -0,0 +1,129 @@ +import { Adapter } from '@auth/core/adapters' +import { and, eq, getTableColumns } from 'drizzle-orm' + +import { db } from '@/drizzle/client' +import { account, session, user } from '@/drizzle/schema' + +export const drizzleAuthAdapter: Adapter = { + async createUser(userToCreate) { + const [, emailDomain] = userToCreate.email.split('@') + + const company = await db.query.company.findFirst({ + where(fields, { eq }) { + return eq(fields.domain, emailDomain) + }, + }) + + if (!company) { + throw new Error('Domain not registered.') + } + + const [drizzleUser] = await db + .insert(user) + .values({ + ...userToCreate, + id: crypto.randomUUID(), + emailVerified: new Date(), + companyId: company.id, + }) + .returning() + + return drizzleUser + }, + + async getUser(id) { + const authUser = await db.query.user.findFirst({ + where(fields, { eq }) { + return eq(fields.id, id) + }, + }) + + return authUser || null + }, + + async getUserByEmail(email) { + const authUser = await db.query.user.findFirst({ + where(fields, { eq }) { + return eq(fields.email, email) + }, + }) + + return authUser || null + }, + + async getUserByAccount({ providerAccountId, provider }) { + const [authUser] = await db + .select({ + user: getTableColumns(user), + }) + .from(user) + .innerJoin(account, eq(account.userId, user.id)) + .where( + and( + eq(account.provider, provider), + eq(account.providerAccountId, providerAccountId), + ), + ) + + return authUser?.user || null + }, + + async updateUser({ id, ...userToUpdate }) { + const [drizzleUser] = await db + .update(user) + .set(userToUpdate) + .where(eq(user.id, id)) + .returning() + + return drizzleUser + }, + + async linkAccount(accountToCreate) { + await db.insert(account).values(accountToCreate) + }, + + async createSession(sessionToCreate) { + const [drizzleSession] = await db + .insert(session) + .values(sessionToCreate) + .returning() + + return drizzleSession + }, + + async getSessionAndUser(sessionToken) { + const drizzleSession = await db.query.session.findFirst({ + where(fields, { eq }) { + return eq(fields.sessionToken, sessionToken) + }, + with: { + user: true, + }, + }) + + if (!drizzleSession) { + return null + } + + const { user, ...session } = drizzleSession + + return { + user, + session, + } + }, + + async updateSession({ sessionToken, ...sessionToUpdate }) { + const [drizzleSession] = await db + .update(session) + .set(sessionToUpdate) + .where(eq(session.sessionToken, sessionToken)) + .returning() + + return drizzleSession + }, + + async deleteSession(sessionToken) { + await db.delete(session).where(eq(session.sessionToken, sessionToken)) + }, +} diff --git a/src/components/create-new-tag-dialog.tsx b/src/components/create-new-tag-dialog.tsx index 08dacad..06c5131 100644 --- a/src/components/create-new-tag-dialog.tsx +++ b/src/components/create-new-tag-dialog.tsx @@ -149,7 +149,7 @@ export function CreateNewTagDialog({