From 035fcd2615b226e58e62ce120772477ae29b20af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Kaiser?= Date: Wed, 3 Feb 2016 21:21:30 +0000 Subject: [PATCH 1/9] Added test for proper handling of trying to call a non-existing Cloud Function --- spec/ParseAPI.spec.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 662e28ed..c75d2ce3 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -648,4 +648,15 @@ describe('miscellaneous', function() { }); }); + it('fails on invalid function', done => { + Parse.Cloud.run('somethingThatDoesDefinitelyNotExist').then((s) => { + fail('This should have never suceeded'); + done(); + }, (e) => { + expect(e.code).toEqual(Parse.Error.SCRIPT_FAILED); + expect(e.message).toEqual('Invalid function.'); + done(); + }); + }); + }); From ce1de0a5efb63011cf57b075ae235f587fe8fff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Kaiser?= Date: Wed, 17 Feb 2016 20:43:09 +0000 Subject: [PATCH 2/9] Cloud Function validation now uses the complete request instead of just the request parameters --- spec/ParseAPI.spec.js | 10 +++++----- src/functions.js | 17 ++++++++--------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 893b1210..52c17fbf 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -587,7 +587,7 @@ describe('miscellaneous', function() { done(); }); }); - + it('test cloud function query parameters', (done) => { Parse.Cloud.define('echoParams', (req, res) => { res.success(req.params); @@ -621,8 +621,8 @@ describe('miscellaneous', function() { // Register a function with validation Parse.Cloud.define('functionWithParameterValidation', (req, res) => { res.success('works'); - }, (params) => { - return params.success === 100; + }, (request) => { + return request.params.success === 100; }); Parse.Cloud.run('functionWithParameterValidation', {"success":100}).then((s) => { @@ -638,8 +638,8 @@ describe('miscellaneous', function() { // Register a function with validation Parse.Cloud.define('functionWithParameterValidationFailure', (req, res) => { res.success('noway'); - }, (params) => { - return params.success === 100; + }, (request) => { + return request.params.success === 100; }); Parse.Cloud.run('functionWithParameterValidationFailure', {"success":500}).then((s) => { diff --git a/src/functions.js b/src/functions.js index c787a814..8e88aa03 100644 --- a/src/functions.js +++ b/src/functions.js @@ -10,10 +10,15 @@ var router = new PromiseRouter(); function handleCloudFunction(req) { if (Parse.Cloud.Functions[req.params.functionName]) { - const params = Object.assign({}, req.body, req.query); - + var request = { + params: Object.assign({}, req.body, req.query), + master: req.auth && req.auth.isMaster, + user: req.auth && req.auth.user, + installationId: req.info.installationId + }; + if (Parse.Cloud.Validators[req.params.functionName]) { - var result = Parse.Cloud.Validators[req.params.functionName](params); + var result = Parse.Cloud.Validators[req.params.functionName](request); if (!result) { throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Validation failed.'); } @@ -21,12 +26,6 @@ function handleCloudFunction(req) { return new Promise(function (resolve, reject) { var response = createResponseObject(resolve, reject); - var request = { - params: params, - master: req.auth && req.auth.isMaster, - user: req.auth && req.auth.user, - installationId: req.info.installationId - }; Parse.Cloud.Functions[req.params.functionName](request, response); }); } else { From 7cc4ef95c08ceac7fbf6575ee7308ab403f2a9a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ramos?= Date: Wed, 17 Feb 2016 14:27:32 -0800 Subject: [PATCH 3/9] Use new Parse Server logo. --- .github/parse-server-logo.png | Bin 0 -> 7573 bytes README.md | 11 +++-------- 2 files changed, 3 insertions(+), 8 deletions(-) create mode 100644 .github/parse-server-logo.png diff --git a/.github/parse-server-logo.png b/.github/parse-server-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..578e5e1f3f5ba2044808b8f30cdd9c0eb33ced91 GIT binary patch literal 7573 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 literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 19bf9e62..475f88fd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -Parse logo +![Parse Server logo](.github/parse-server-logo.png?raw=true) -## Parse Server +## Open Source Parse API Server [![Build Status](https://img.shields.io/travis/ParsePlatform/parse-server/master.svg?style=flat)](https://travis-ci.org/ParsePlatform/parse-server) [![Coverage Status](https://img.shields.io/codecov/c/github/ParsePlatform/parse-server/master.svg)](https://codecov.io/github/ParsePlatform/parse-server?branch=master) @@ -18,14 +18,9 @@ Documentation for Parse Server is available in the [wiki](https://github.com/Par If you're interested in developing for Parse Server, the [Development guide](https://github.com/ParsePlatform/parse-server/wiki/Development-Guide) will help you get set up. -### Example Project - -Check out the [parse-server-example project](https://github.com/ParsePlatform/parse-server-example) repository for an example of a Node.js application that uses the parse-server module on Express. - ### Migration Guide -Migrate your existing Parse apps to your own Parse Server. The hosted version of Parse will be fully retired on January 28th, 2017. If you are planning to migrate an app, you need to begin work as soon as possible. Learn more in the [Migration guide](https://github.com/ParsePlatform/parse-server/wiki/Migrating-an-Existing-Parse-App). - +The hosted version of Parse will be fully retired on January 28th, 2017. If you are planning to migrate an app, you need to begin work as soon as possible. Learn more in the [Migration guide](https://github.com/ParsePlatform/parse-server/wiki/Migrating-an-Existing-Parse-App). --- From d668bd984cc4508a47b0838d1a3084b52660c281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ramos?= Date: Wed, 17 Feb 2016 17:25:24 -0800 Subject: [PATCH 4/9] Remove padding around logo --- .github/parse-server-logo.png | Bin 7573 -> 11622 bytes README.md | 2 -- 2 files changed, 2 deletions(-) diff --git a/.github/parse-server-logo.png b/.github/parse-server-logo.png index 578e5e1f3f5ba2044808b8f30cdd9c0eb33ced91..80fbc24344f56e09a15f288b60b19d2467602393 100644 GIT binary patch literal 11622 zcmZvibzIY5^#5tbsL>4rMUfgU%|R>5nH%l--Cm$yllQ<9z28+7AuoBjgmi;^Y z{*xG!jfaPeFa+Y|<;Cg6%jxW94dE6N5`u8?KzMjK?kzaneVjbZyg8iQAN-l*-+81h z-7VZ~T|8`^oxs26H8Xei^bliW`t9hy*WWlDUH)_AVJp#e~tS6&7WcaWcA0H=)E+;Qf`)J9?ouB&dv_vu;1E&6&%g1Ej297UigSY{$Gp# z9jWN=DTUSCY%T9|{hg6Gw-%dj3i+Mx|B?P5ZU5=L7grp3@9E!#5(hrv92P@C zVN+0$hG}`D9{8&`JXC-iTP5?d<;lYjhsssg?I^|+z@D`z14g*LPtn(Vuy^|G92-$k z(IR|_F=jHT0TF}(T!vji_~&@VmsSjMW(;_gNHcrz&Lh*9AQU4$kk9yt^HUrEu~kIs z&S7la!?GG4rsGbYSjhPPm$*C%NJm_p_tEMn+|d*z5C{VU6$bquE)-}TY%qq^&b$37 zFtXhd0wg5u>;KM3etJ(=>hXA`Za$I#CB3 zgoTyFfRBW;fcB@a(>P(yyE5Z^ry&D6YF>}{Cn(%Pg1}$^04)T%MYP=(RCTjbcJNK| z@N?qZ2MD3ArD>qL|BBr0$EDK|ao;xg>X73i`F~?YXk!Sof!mI{%ks&A1O_r6jb?qR!E35_e*-&ZrwCSV9%og z%s&bUCqf@%U;wdJ*lB%o0~dTg@=%7s*86!!8KM!dP!lk4(EbVclsGsQ2=J%C`|50n z5zfbS%>FxrAn=$D#&aexORtm-Ue#+P+$6a6NCnotbQ)CbCh1F1ty3kic!ny^;l z&pXA9bPPh@eu!znS#r3PTb0OXaZ5J-Qve7@3~CaPP1MDua*k)!61+&*c#%Ji7OQ4` z?5OeK{yze^j1t+rhz{Ojh6*lcwF0mQ@f~oL2uP^yIpq|h{;cM`7Wf!;-@xjE%P3L1 z6?dHBF^62oH~l8J4%UWAaW5$TtrR;s#B)?wSm^4ZTv*s@h62IgMcX8LuL1?FAg%|A z8S_Qq$(nz|qr$Kw0!a(Lzjc$Af1i-A{?@uFdi`ht?{7?CKz-xW;GVhO$SV_)kB3?AN(eXrUyhfGHNk zMgOS^FrY3LYK97%r!x#1?hux2!8#<7IDX7K`X=C~hjobc+NVpWMQ`k23IfrkNYly1 zWJStf60S$qFASOfy)lD<08(<$qTk&H)fbU+^-aZ^V?-ervR_+_;&^5k`G{6hm+~!B zE56R=@jU(=l)%f5(vlJKrzoq!pp2;biAuPncviU*cIJ;pPDrm=gfUCwZ8e!7m!Hzr z=yTcIg>e3R3{Xph8-oz(m}`KySz4_!L?F>^aW z*uNTgg+V#-L-f7!WCER;zE&eCbQ>8e&A)f(eMr7l>-4U1pXM$Pr~6wI{4%DzYVAFG zf+0XLwXfRme2zY)V9S-6hYX^Ch_Qxgy(;PeOyKg}4aMe=Ak5%H315D7k(^h3=hxkS zUq2^uwta3zUYlMXL_F2|v~HQp&`g1G%(P7V)3;DeFmluM)%jY%L6?2L*Sg#aN(0cB zN}TYuGX^D&>RT2;Hgj|YKTh7dKaS5t#c_GW{C66kOwYoT_T`v)<)=^EV@K%^4F!{x z>cC?{7Zff#*Qk{XzzGp_u zIU7ES_(zN{Fi{_)T^b0=^!Qtwdxa<|tE+a-p7wqH@!eD*22NB31iakpDTC{=YU+3V z^bxa3RF@>`1`m%3!O8GWf%kz$RXm_DG4wqnGL6|JNV zewybm7DD*iE6YrJ&Y!mlI8_7C_2S;n!L_t)OeKV4!`H-s!XC&%>r?G8^l95X0j$&D zg|awGS@jDw3m1=4!M3HH%H} z%5mpIpv)NT0iel&@YJZMnVHt?Gj-T@T$%O9W{2)2-3_h5)4#?9Xs4t4Ijtvs@R;q{ zCt$3!lHCHPG~j7zFK39(OTkx$MABhrWpSk$gQs%8V= zs@)HPD<`HufPT*Nu|4RQa_{GDd@3oM$uR8Xz?#spE8jBelc|w?QCRMf;9Yiy4pv&n zk|G_~MWXtx;IP#3P=)V@(Kf=AE#Uxv6I<_M&#}^QEZXkZVxU;o1$+qg#Q3O+i{{0zU8u15uiKdz$_q31N$h=ptUYWoUWEec@luQuXI*pg%l7id% zwO3ASGWNITNv(5M4|vlTm;7?6^aREZ8zSDx*9$HO-kze;J-=B+(eb`>+U}P%%SnNW*lcrl;)lIl zG`ejReX289H-PP>pbVxnfO0Tw|E5T(v*CxKb+Dk5i(wmTNiX?sE)AOgj_G zobYbj>By8XO-P%g1}~hc<3&kx(T|d-;q*V6#w82$xF}ZQ@-i5fd0dW;_YOPfa(s48 zKLp}xyrJ*#OVOpakaT)0XOnlBKl0T3f`;@aP~vVjRKh}eR>?D2lxgui649wdk6diC z9MVH`IWMqyP9;j}W;?P_*5ZxS$g#ACt(3B>P;2#AtzxM5ooVl(B8t5d&)opsNddBz zjO^I~tyI}_bv3rRs{(HhUWyIQv(5LOJGC(`o{XUf+5mBNqOd-DB{sMu<7IhAxSqcS zN#sQ*Y&%5kp4d8`H@J-%e>kQ`d4D7!d9&Iv7$Gqy+UXfCahdrx*CKXx^z!imdLw0- z2FHWjGc5ze^Vl694je#T!^XRX4K_#S?Cw1$S=}|5<01R!tayIo0dn9nIEpZ`)*xlf z5~#DlENi;WLdeFq!QqG957J6lQCj=eNCV6Fv_ebc;b+N`0A>De~7(QY*57qg0 zwIK&0fDWn*K_hRvVWd9TU3Y5Z+-b?SsX=_b&llWeds->i#if5e_A$z$+5PJ6l7w2X zVCCT}@d?^F*5Q`c9DaUE00-oBk=;ZM`U#zEs1_S8jJ_t~>3FhNPm?GS{hY5QBrW1V z=ql!>TWj^Hab+AqEv63n9^B&{_MWTl^$yHEB4Bg&+u;b|$ZRS+k`;g5um0M4O!F#6D1L$oH{;am_yHJ$F}^TX5p#gLQr{ zc7dKu*IVv(S2yTr94C?d?G6f1kL^UE0(JL>ZA`c)PxC7@OH{{9xSZr7!>tR3YTyH` zF3eXtQJWNyQhF;}u<$27ATm&74mCDxa)tmO-NKs)4)gUS~|9vyiy<&{Lg6Jow{ zwmc_*-xB&IyKgX6mWPvm+l(ZDCjbXc`=~#?Ote2&`-k=9Ly1a_ebhR?D_vsHeIk?j z&v_c6BABxD_Xeq1Xi6o^UNk?lV&hVxY;8U|d-d)zGRuhThD(c__@d(?vfDYt9O#v$ zS3dl9+nMM#w7jXIXLGmC#5!~GQMsyoRJr4;!D`RxdH_d-f0n715NGme&h)!JhkaFR zRoHyIu6J;iZm`66YvU}XV}de*;tX!h_X0-g#Q|?Nj=99w1Lt2izqnw$%yb=S!=1gL zFFS4ogS9YygImeO96ly^J=bb!o+;*|(L*oFjJ^Fh7E~I#@S{@zS5EulZMAVu531FE z9@H8MDMf1WA7;C#$9AP`VoQch6!{3Naf~7l%nHyuY;>C(l_sQV2Meo!bM-(AueQs6 zL-HnH&VchvcF~GZ2^->3hoVmMG_UMFIPP2@PI!gG>eBLyORn5s^L9?rCt@}lPu8_5 z=qWJ5>MTpb!$)MF;Ql0Y^{U-bndcO4sB>zayfUC?K4vYod@myh{g4){b)fMX0y4>8 zI5pXz80aoVbh#58jP(yNP*x3tNNyVtePSx~bC(+ltliKQa~8Zl+_)loa3Nh$$gzZQ8ilCTtbXrTmfL;nI+T=7 z@x~`5WF^>lm=9!hl@5#IX`BaN(>z8q;?{TLnG403#E|)UOm(Oj#AfKcsWQ=udco}m zPfR$%_(W|@_CkSSKxEZFdXSm6N~$yCU0J=f5*_i^_^k=cg(c~vyX(PsB5FLVT4OY$ z9k>2V=XWTCcf^6==}$Vmq|SR(gZu5Cs2yYtraZE?H?(`d>eUq%{3Lsxv$?j%)pf@< zMi@_J*GBGEa{S9OL_peeJvR3ZeA#6=?|w%cdFjIzS8pjL2h$s`6c`c3J)k}GA~aK4 z$`R1*7>bdigwzkI7az*pV8=+F1*Y$ahYZwpFS)|geyQ``Dx#AfB7LmI0pod602ZMEd^ z8t8wgLfY)OmebeEFIQ6xCdC_f3-SkHz3HBLVbZJ09J=ZJB8(aKlqUCX%|xvJO5oyn zr}r)#@A`ziLEh@XMUiFC1fHheE3SwM5j}3bu$qvlX=a>8-|H$O$Cq?Ss%_#kVQGqp zqz%>7_`-=lRrToRXy`;v1qbMWP6S7P7)~AVVeO!busFh7t+AQ}z>f^f*Sd&6{Ujlq z>AAo7th|b2%nPG2(h7gO+$4?H32Vd6uO?I2LE0G-zJ$dBX|Qfg6mohUNV@h79gS)b z_tG!V2-he4PG_drQNf)?29;EDjwpO^zH1VnUktEnlDbEHA;ND+eTTV7;Q)F8V!I=_ z9)59H86BaWWBJ7c(mSO*&=GzzCn_k1)LTAum8ceqr+E%XHfVXy>>=X%O~_Bz0tYog z?Vi4QuAzii&O9OR*v+)eJ2{xsBgt8z3z)lM-4Qotak_jd{{zr9EW-RThTcl**Y& z$9sS0DQ;CT77Pl;0{OjV)e9Ok7Agh9eA=U~+D*i-Kevnpl;OgTTgm0hj*TO`}~yvZ+i z-tQIqoL-;ky6n6;p&L980Ah!-Ufl2Z8((Y`gU~!uYzN1X<^3}@j-sCYs(L^A65w&{ ztD(ZYveTq1m+x5*=(}+OEJaeTr^-yfAFw>k9kzm?ZNqVZ=~(qjUnlTzo)nU7`f!$YD^2_;N_u9 zrSB-juw&`7Ya|X2sz!Y^XKwSX;H{lYmPKvU;mZx~F2l)g3rgugeEkO8H9n)Qj%yM; z#TojGWr$-m?kAxU`<1gU$>Nk700Xd#p1S4&z5*dj}5+zxJpG+r%V=|>$-F*Ur!lD<#CHYSH9r5_W|#Cds@X1feo`Cb~Z8FUG6t@SH+(gctaxLnQ> zEtVXiXZNmTy&D-8Tbef@xZn#xN0}!81%!=kL0+9{dIa3nC-i!I8^@OLK7d5cbC!^6 zYwxs3kedVY;pG)`YF!?=4Q0D&mxRCGCUQl4I~XJn)XKB~7rE6q&a$6?+$4nIycZq% zSQ#_hB^#K%&>_FfzD*jztT{A7?DNtKsSGdw{sudK@^5OW`P_oX#KlEzR~{1WWR6cE zQ^`A}<*{k(JP!=~#y*QsaYumtTz3m<{VJ4%ZuId$BsJMoE?o2Ks+p2MgO7!Ts+6nK zAO2W|U|oBaFn=DHORSS!ktmS5D&s=l`s-DoalL%OHqqcXn06>oxz`o?tU?7*wxP+v zf=ATmYY>U{bU_w}v6~l&lOR=aVSL!D&z(*M-lpcK8>44Hg##V>aCPzHl8`)^^$V-5 z*Xa3&YvZ%zE$#98AP~wT<_ng+{-yb>>D~)|VN2TR%jHQK)v_AdN)PROGZU?%@!0FC zpBJk;p|v$_8kA$X#MlVeAPJ96BKVn|kj(e|pC`>xLm{VSErKZ<#Asjco}tnU2zA+4 z^pIUMUpv2f_X*A8(DJLaI&?#>x~O$d+)I}f4~UZyZv3L;^HF8P)z?R{)2%iyPdhDu z(h+{G*CQ$h_|30z9E*;=zk-v0PfM>z*ZsV5?E6H`2sJij%b&_2ra-P=Br<7yfvI zhG-esJ#HN#6HU`QgH|1gvmcJKtt#%-EMPt({lIN0O_W9iWd_%~>nGxu2TTcGK>E#b z2CccR)2xe$O!cIGi~J2VYae<`PH#fIfuFIgJhPow845qQF z^9S+%jE`UDyU{57>c}fWgE)DWCqT7pt(R_tYwc>_?eeS+=!k1A^UGe?-wm0(7}4MT z{xt&Xh!%$Nq3~7LS~g9#>)ee5U%ac{D8407B7gHSQbm>oxc=fbk;68 z%aOF;k4Fd<^u?ghixL_ZL$QCcK)NdnC*VBq^b3#--QS#-M<|5J?&`*pMA)p& zbz>Q}MqCn!zoHN+0UqlQ-yeH$hgos?R_ULL>ZeWk6q(YV0pmtsTe9e1L#euIs7c}F1Qw8b%f?$4?7 zz5tOTTB#5=N1XOBZHXD9X+5Yr{wCa&C-}Pn?qO7@Is@iZ(U)(O%eq!Gqq*ORR>a~6qov0qzv)ZtjrLDW=m#JbooW#IbJsqtmd6GP>%g z5L;`@*4OV84MJL*u2=<|;Jib5jmyHH2kP5EmqeWu6ZK{LG|JUtgeJrwTGPpA-zogb z2v|}zETG-cG90fxltT7+fw1P9w)#{xU#{AAY){$NaU2zcICvqguv`cNIe-5@UaIOe zGq$4Z9syC|L_U=X}w0HeBJPx1cy^Zyp3J7H!bB$8$i}SaJJeWdqA#8`QV| zRp+a1;UmbmDPPAvh`in%7-7Z;H<62*ibS(|PgUx26}Kx+pgRaiC(KtFl62C^bC|vA zgjx^NHw~GVB=elbtqo&yZ0H%gTrpMw!Q|wv4E3g=P`J;jwM9gXpv~h%7!$T7b*gDZ z+joJmZIE8^57PYbPn{uWMitd~ga#?oz&#sSf;;L+t>}bj9V?p|ZdO>J*a`T8+-5zA zK^f^!|Az7`6p7&~rRm4eQ<6D{d`pygRMCzvlxFr7`@pYtMTJX=-*_fVI4sLoYkF)% zzG;o9@;lx_bI?XOo5yM&v+1{EG{)ttevEM&mKe3Is6o8FsJrAK=oEfe69#9iXJg{N zd&8mPvsP5J^3k0MLT=qbOIQ$HB=e=Q^z5{$QK=pURA1MFdCyanAE)JHkRz^E^Hb2z z{N~kLZmb#C1N5@yE?M)LhGmPp^qJvfSEG;+{STWfFVfiB4|C877m22JsOD&8Y?N}a zQN2dVYwRhC`b#O^&zL>mSk1vD!oXOAt{NIQ6)3e5ZFn4TZvSv%TU=dq&cCP>_VOB3 z!ebrj*z`dgru^n4rQ4L;0%Fo^CJjj@fiRQWGNO|8BD;?1%5o?(&i= z(D*qK?d0}d7mvZ`FVin6tzHM7_hCZ<8|}Im&~T{dWp=t6dc{p)WBsOr`DV_~=Ja-2 z$9`$3BV2i^IuX@wNiE!t3zaq2tqM8gYF20MbDz_)jMiixD|5#BTWo4!gvT@eRI+qF z;O-|5wpDK4oTc)inwsNcyLF)Ptk*PO!VGq{jSdbX60Csp1;3&AHt#Vqn|(;=Ofncx z63kL+66H=l*NK5naN)zxVHb_dD~gA4CH8(d5vg*6*`evlv{rn0Bb1ICbBuq5ZvQx% z=lz^<%V|sbv-UFH{oX1s;Any%s%Jm5I(urjDo-0uBo`k2u4Vs`#@i{3{ja)Ip1#-@ zd1V8MaKb#R$H*Skuk_%9e7y<(^jKr*!(8%1E(3a3kUpM~w`3rlKSsf>iC-hp>l%e+ z6pFBMP%8@k_k{}y)St28#KLEYbQ8k^u26g5*3r+)Fcx}Az?J25L*io0&kv~AtZhS* zJ&H|$-U!!_v!0C#uOugP#d%rp;vf)(gOU4_Ap?TMz~*%FFRF}RAFHc-n@b`uGZ@be%W!@X;@BJcuzuQ)_WF7SuWmxGq!Xy(EH@JKL$9IzJHvEI(dEH08KYY6Ns#a7~ zLdmR4XasL*52FrUgI+HCdt%P{`MG4GhVGhmOe&PFl9Xpla^ngXLZbxkLfXcWOA(z6 z-o61CFb$4K58S}mASlXk+83OmvWxm=@?l?=FRT}pWy&wn%VVD}?YNv33)@J^%w)c; z%{I5DEV(Odu zQR8#`@Dz`T*pPQbc0K2YA;HW>7v?ROOS@KeG=ebT3{Xna9iCsIPRxsN*2=eo5`bHp?h1mQ(|O^=A7ASiyl``P0lN@rZ*ow@SfjO2@< zZv}vKifVf8BH^<9L2r4oYNaWLI8zCj9Mw_cIbLs+hha{%ss;ktIh??9(~lITwK%>t zG2CgQjn?~Wq0>@+5eE6VvExuHU;d18v~afBMLWv&?HB{tsPdLa#yODSL`y4>;e>LS zbGq{4STZ@!67}wZXnJ>Gqw@s~CI+T_A-fZa1M7{f@%HxXGDLWypjV(yk#c_}5fzY-68|di7;n#L;=mmVjflITFSRF#(;8XWzT9A;I2JhfX!u-I7CE+O~>ij)nitGCfU501y%@ArPEGHgZVpc=9;P z-M?3FR)nGthT^y!sXi{@T&3Fe<*uYcjTg1xS1)nqEdAhXFS=g$eJlA=qnAF6j9n(H zi5pm|vL-{3ALj4lh|+$#i(X>rT>AVJC0AjvNO&zWh=S+yP&vAhb-?`b*+W%PbgMR& ziZdE;>E-fA%_n)Hf=wUvsROKK!b8_n$D(kTw(0S{FRJv|U02*X;qvqfAU~xVRR{b! z-Oy>vtaOv_Icl)w26y(rV9J@Nsd8lz&rj~Y9K0|R4}t7Vq|{h^S4Q5vdt8z|C^%u+ z1HW`!o-DZmeU+K{<&@bSNbXf4F_yEyR7vE3#k+tNCLcl(a&Z(*QI{vB!Od!nAMbL9 zQ3TbfN+&QPD7C!m5bE`7h0GTg%--u}h+w{Q66;?dBlI!Fz_mGK74yvL74o4Waz>IY zKD|)wbddYVxh(xJ6@CCb2HORx;FapC;_XCOT79DAx6zH`SX-vfD zMXK&=SCU&)+6Qb`28$D_?eHyBuFVx>jZD;TDGBswA7wkhcvIERt2{lui+PSC`Z-&D z=!3or&f+bbP8}rLp4zT{UoZdo+~mhn=luae&BaD}d2~X0KhIUi9X88JBk~*+Sm*XM za4G40H0lN$K$TJ>P70OyCL?N<{}kyG$2~e?`pAw{Gcz?@tYO{=y3oAbU^4;#tksb! z{^;;!r~xKE{JGsD!N$;H+lXHkr6gFvUNv>NZP8cPBatGpFLTLvuyr-Kog9DKJ0y2Nl~p|OmALD z-1%z3Ky8<;Nk(zg~YxsfWIX~Do+jV7fi-l%R2RcggLEgz*7+5qUZOhQc zXLa};t7C*2w{e(h{APxNf5H&2g1YRU@>*;?|ALZ+IGd;^ws`E_wHDMoeM{g$(i-+` z3r5%XC@c5+FRlYXYX-5fgKa{)aMkk~qlUG0bEPL)AEpf{ywqR4-%AmW+E;MM@4mT? zF*6SdyZ*~p)y3aKKEZg??UQ5hBHw59bZ0{c1BaNSJ%xXesIED%x+2u{z;*NDiJG+m z%|l@=qS^Yf{B*|hfBd?r5q)ltWsFZ0$QDtH9CWwDsQfvLmWqS(ADo9^{|zLO1JGMg z12}ykIvfDQlh*>=ishI}j7)zh9Cg%SMKm<*8lbg>j;XI$ZBP1z0*RCNHxiDL)W4iv z3gC4EEW?`h%%3oD5&ys;17dG<=!2q6L9nH(<5y(-7jOCmdS3{WkBPBvzvc2tTQUbk zxv1ko6gd8pZlY-Q0Kf}gxLe`Kr^-)s?kC!ZvN%k%9&2I%ioY|&0w!xda;a&g(QJ-a zEycQ+_RE?7<=dV|LLI5Y3W0`B2xjqInUq&SHP^lzJ>5Jk#D5bRqx+4IJ=C7y%zW(K zL1_Kx;nc`USp&}(r*Khq1?s;Aq|G>%5%fxHd-%MQ-ZwWeTXbp5$zejtN{PNIymUUD_i1%f)u( zg|DlZg0)6Xp$lZOUh7XK)x!|{sEn}v3hNK5F4-r5(Y+5UpbFUW+=k9?KldeS4XEP; zgYssAxJ8Y0<}v1|FW%fDYx_kg#Ow)sD*r{}5Fp*?pjW1&mXR*xFm)tBN|pGX23Lm9 z?RMEakreT7Qr`dJanD)750Qa)vt_X5$leY+QfV&v#ln9pt{fLF2LvMKDtVT1e+;yw zxg~tsCCm5^)yRi6K=az#==*yB!ERe_oT#Uz;o9Yg-0j616=(Kayv?PI zh5NnoZw|AZ1TBXQMEuHj>_UH=&eqH<2&rvDZVY9~xOlUeNlE|6SvKJB5>8VEkKh7q zkv^1K%f9FiskGo9D1ogzd}Mi$q`*InXEZy5mb7Ng-?{Ih$$KVLkELHWVX=WCoRrdZ zZ=0dZ#YKeez)yEwhD2Nr=0X&{_-VNKX{u$YPuTijBJqDBQ~!M?KtWuL$UD^H{ly-W Tx4Y2aM6`m8s&u8KX~_Qqk&1Zm literal 7573 zcmb7}XH-+$+V2Ah0#Z~2lxo?EbWxh2qsXR9OCVI~y(pmuf+C`X;+7^LARslNOEGjb zbm={WNDVci2munv%f9!VJNAe7jB)OVwboq!@y!48j5*g>bFTG!_ta3Eot2Lj006M- z{Q1}f0HC)xYi~0%otTKft%qhgb z$5|Nw5LD24tYH>Bvxzfu`@=F@cze}_W6rc>r+p(ywvjdRLfZ7!nf5Q z+ODNpO?!x*#@xTPu=i4Z+@`d@n^*c)%g!8vtQtxt+AsmsgH$h90s!YE006oR0QENd z|8rdYm-HXxv&er({~h`7=>IJLm%jf<_uu*cvx>hv|HI(#l>aX5AM*e6MTt_Z;HTgk zYL{WlRu^q1ktS6Wmy=eN1t%YgOzK_%oI{mFmt4tqsVYz-1yXsyqHdVFeuhbt5{Ae~ zxjOX}NFIlSUheuZQ{bX(qx%-HhhefHpB_Lz@ib~&S#x|;5?hOaDH&!GAD`9n=3?o= zu5ipzYR7m!-RABFGel3D^DKoo8wddllOIa-OFgg~q6g6FRbKvjHU)gfl{4?`_Z_|n z%P^LGquT)tB_?vWb9Rg?H>ST>$GlZrNrF-2X{Za@0swRo!2_Zf;eSjrkFYVa8y*M166XGrpFkM*nVe?05Ds0q`W_KN%gz!L>C!(+nU7^&=$b(uiJ6&hWp%)>h_J{)GJ1u$o{Dvfgze)9ITEmF zfIU5(IJ!crW}d^t=$XegY>1FUDz~aHU$wAL@lE5Qk4T8 z=0chr^46g6XcUrKvvs7~4$8jeI{h=Lb9!blrEkhafj06;{JMRap}<<6>gx&Qn4}^y zbT_uj^t}Il{hXbHPhjM?DiaG}$pa=a%?Sk+SZ92hdBeePCvbT_OnPrHV9l%@Fc^P+ zC##rZwy*UGoDg63+WkB#Km&g;fxPnOp!7A8nH9kdhCAvH>dm)F*d@w;tN#4uBn-1*Xhafrk2o#I$u4ueqU|n*tqFy&~s% zF-FYC>-z}Q$y9N`Bs%V4WpBSosQ!$uWFfv2!&Z0OT9ACT9JC)%{^c1TohP&1f!IsO zbf^*q#_DdD5{+u$3dijb!ufEYOtP3;xt`cPB(ppD3Z}*&mB+%;MEqQ}) zxtSLm;bjid74F&)cG^~VxT>CVd?epO=-YG|Kth#_2VE`6oXux%lFB4qoG4q}PWkRO zo!a+2C-}%aGE#_sTb>csS7@a0gttGh10xczmg&79s(S~i`HQ2ZLPt67Zy*sm?zz3$ufI5nJ!*NXoC> zEG3^$xHv~!+*QTh+hl<}#s5MxJF-T4!$T`2P+m8$r`r6M4htR(pu0LwKeYCWo9=Rmy+=z~8k@wJenj=(`P3i?2S`X*tP)QTXe9%@$ls zANlT*8`jqzlywSk;r3L%^{b*r>kV*o&U72a`M{j@*_(y0rq(_;ktcd7<%53Bpt(Nw zv7IcX^>(+^{@B5K;AB5*`7?s~o$iRwnK2tTq_DC-WGrgvHW!b0BHnaKk}cbg?Ts&2 ziV5Nyh6H|qoc(r^YT=E1UtwnCAEx)PA=?AW$I_V?QR%bL#7_3NB zwxk?so%+I)Xw*I!#psAaZHhwlKZBLySSdRar#5A`OX{dkDs+a>KE^9Z{zS3SkuRjl zzGASp@nD5#6S%a7T(-F#HGU^gwcjxeHt1RC2yb8&lzA9n{OeQDrC!}R0ucQInF^o& zIZtj0^CYB09vn_e(!};1&3W_8&D&<4>h;UtMZV>Ce~E2N3eVpZ({uiqwyf12z2M*D zzv^&tC%<7;t0eUw#jm&C6`rI6< z8gaB#-`3`N#L4;tXtPHLn4itk8R}QpzB~7Bml%3;*;Vj0%JOyVo;-z@(4Xi3(nyK< zmKF{UCTGbxX?o4_IFShfC5ONTBy2f;9;T6<%>`8hxRS z*zM*t75OL1@u@|H2RE(bBewc;YwhE6<(!gUI&v*)xm@F1)9NeF)ry&4FOqAinGEu> zvE4*Q6w1~Sy}XgTOAGm^e31hWG4cEWe_NmHPJ)xn(%|~u#&sAB!Lv`T{Endb+tG=8 zPN`VD2*(yK)ABF|T6n!5-toEY&+&b3URF%a36CeA!NWz9_OMY!lZBTR`+FO&5iIvo zQPtN!7u`t1y~O_;#0#(Prpi-U#lkr8wo5^%!X@F+vKrPt>}v*UlQ=he-|(P5&9mglus3l6u>LAY-fW$;6+)$YNDA~F&f2gNiE_M$=EeEW*oDk z6HUTs3^U$))q~Qk?^F(Ji%t`_o&}HK;aN1FV-!ND%Zg}L98@qG@>ozQZjB|DewR|- z!Qm?~m=_^QVwq7Q!$?ND9ls=y4{Z!y_jlb^;0W5n8%OV|-7RJC+{-TWITYfaHQR(L zt{h6VsEIHxs}-}|Tf$_Qz(u5+RIUA#eP)|&-FuTHIPWg}cqi&!3&0FabTRf2!YTVb2eWm^Zf*R2Z9=;Dqu`xrh zcA%OQaG^3O)DycpujZqW9+?bq!<6hbVbegFkRfxvJiTfnFt3Ez($lvJWCsx)fH8-L z$>9f4=)E_XCNQr`cD>=P00}FBs6A9L$~FloBhC5;IeD=OMU=lwGm~;vNreOJD4CFS z&9m7?Z=+Ys(S+rg&fxxDsp>w8A*h7=OMYrWi)%N0?z!?X9=|Ok)>tgZny{k++{e#965#5~XDuD5OW@xP?9zD5tvaM6HAk{0vbSW5XoqYrC}!?q6- z;4hv&=kTqOayCAy54pT|vt!GxiwXWoh7rL&2Tk;#|F)VT&QG1D?5cAc>Uv8lR|wfx z9%v+X6&LgSo-Knbdy@5QZ*FSZ0q49YJoJ}S(41-PQK0D^>)&Q`iF|c!Hcmg8y1^VO zlIvkf#P+$~R+RX+XW=1A2Y$Eh4Zb5;9W^<)O_%-519?y7`45f}U_r?kV-OSVbJVxE z4BO8$3DctMF4NtQyNCCa)(MthyV2O26gg7s}aMm z(Gd~&CmfsKiFr&;E-KUKVP`1cz7NRJyZL5`*2ew9AVqh}xh_FiQ(moFX0o|3^z*8F z$Jhu6IDvdd{uR+vMO=7uwq41nn;i;y(w6Z#&8m{qJhP3LV=_wwZ8>{waDiCai&^)s zOJ@V#8_X{c)#$M>KeYF zecO3)$A!Dq$T8?sikg81&*NSQ+K_LIyur~67kSjS(ER%g#GT7)cI^Nc3a!2E{DDVo zOX+Z+(W^ODX*5gmB@vai`ldRs9f>-;M8Rr^*SeixsRVdWJ$yLN9Er*#Zm^u29k+g> zeW`jyvSq`)8H2j+lrr8WUCr{=ojn#Z!%@9Ee^pCkwT0MT-9~$^=X1=6k_<{`x340w z>q_25h?N0`uQUgLc9dtVnCTb)DsM`Wkg|v()y1bKt0iok5k4iiEVjn+RJPvDSj+Oz z5bv1KI9FE0ZYvgz>I*|ReD#6(Szo*xG)ktnlw1~5{^UPndMUHQFTw-c0jd(&BIH7w7lcm8})kZxUl>DNau8MlARWU{sqfU8>IEs zt06C$eWO@>xmIo3ef$SLB6Zx?Mpa7D5T;nclg>-UY4lCbCBKLX7obCMz7$iDOm>+* zl~8jhj-@wLuQ+_ZA{=9OTATd_VP6~fbl+=JLqQXiS%i(aqOn2yG4t9v*DFbi$xpd_ zG~S|RYngV`^kirzKE}ZZ`}^BcD5CeE`U{7EEVU7F-u6Z$2U``wZ&YM3>CeqHs4E7? z#64lUG4uQjK1+9TmUx$@CEvbt(OoBlSsOlc^V|ujXGu5$5&s4E3br=Z+tyk4Ix;?`i<<4=0=707hPLuc~?kXR!4Y49nf((eeyxQJF?&`KPWe; zaVF-gO$F(Z^~Lk#A?-LWIh%$g7p(_IdS9S zaH>xT=|zpcdN)p6K&Y;u04^RVXPrB@IbOl-ZPe_b&@UAx;qnptvELTZz)|j+Dvhy6 z0!0&}J#~M2(EXY8kcj!U*eA+ucp5TlA_PzNU0bWeCtf&cWTgcV4nC8!Q_a&e>K@cr zgypf30xAQ`eXuGP0SUM=^wK4Ikfrl_$J*2H=4f$UNzf&f$pc!UFj+10m{$T)5vbQO zu3tZHcm9jrGM2Oqr~0>?RIt*Pa&SAe*zAQ2(d7Y2y%q)v7pg1m_P$>PcnVDjHJiBb zG|)76;DUwKrOFYE&%JpuA+nLAxx2ES*x5v8@*r_60?6FEjY=v*y+H&>f`6;G&kso-58p?3`+$5_(HvXjY!=R> z93OgK2D@|3??#GP96!v1F35F#XVK#16BvW-CY3OuuC8V?F_4FKUsyjk!6ve{BG;KM zxK-AwU{BQQZq_b5YNoh9+oSxn6cg z*p=KsnF8quYouWf<$Pt)b17C8Xtt2DXhX@L%kX4_`rH1FZ-7Z*!Xlj=>CYE`)Lbd| zNGVb(bcpZk@}frb)|r(TIV+gn;-faQLi)m7AQfRrmScI(p@SjSUN`&GhS%lgv?QMz z>2+-SgX0fw_d}TFs#|OJRbX?#YthUYN|k`&LR(+E}0R zK2#3I%Po6nVHWfEt5MX}_0sGo;pq|H9za;_(Kg8VONab1krfSG#TI(54UdtD~yBvvrE}SH%SR zRoE~iv8R;#wv;Tv*yvzVO2W~i^_~jjA@)+;$bE&@9{4F~lgLxtWm)Ufj=v(A!W9)a z%1ASao}961+{U*UXLJ}Cr`KE7YOI$u4T{^o%_&4A2AsZf@M|r$dT_ntq9W>;c^;Wo-}CM7Vr45*d;fvHsdR z(04%KPHgu_yss@2+GW8J)(qvGg7%iaf`0H6%@-P=T8o_ zg9m-iIp)mnj&{{;{Y&J@(Q@Ld-Eu{} z+B~SE20ibwrT7K?Ee)r9GNyesWHl6;?z?xWKd8jLdTnLHDS^)CgK%vFl#BCT!CRd}I4&0ygpd;g{zr+W}*bP82Q0X?2p24d~KT zdh?VvX|9={8uQk&tnn(_C!!|#_~T1up`eq{AJ8?imLHj=3`p%&QZsfY6_gXK=_eQ6 z=F@}uV|**=Lszk9_}s_VwH8Mxt}9v`KB(3Wr#@>r!NDk*(?3mJv^FlB;$alwv-U>= z!cZyNH*7lQ=rahq+9TMt03^!UT;MZ3R#OoY&RSBHv3Olk`texBS)|L+9`&nuOUM)== z;21ALpSQ9B*qosWR9Vwg$!g+C+OcI)de(&l@AP=e@MKM$jRN4_9pS{)nlI{O10j(l!z$xzO?Weap~l%yg4ON zL$r#Ut4KFL=-!>NpT43fWc2y;%gbVl=~Wanz?9R%UH!;;0-u-cIvpeO4rEfSJMZD4 z=W%t417ON*aj6wb|NaJsfA{;jJhXw?Ds{syV1 zLp0dO-w05=vM37cc7vuX&315~@dDf%l&sFGYdTSLo)r)(|4ym{ioOsn4p_x@nNr$@ zZ4~>yI&p`HThpn(I3o^_cekOb)3Ot6DE@MnY@TxfI;6o6Z~(4s$~~vIAtCgUz-7M=+xWRAGj6BvGGpJLTbK3&QQg^ z{S9&UNVVQhC3*4q+6=<{`p7U7fNmo`IP!vDU8aAUC_VBX zzKlAfG&3`wYtv|oag}nfWgr!MTNagP1jqC4Gx2DYZ1eXzsIaY*g3%+P%)!5F%VjFd zVurY9I%m311AqFs6sXDg37+XPOywtMx*mLP>rir}$(&Fn38kIYB{x2l(lKFpu3RF5 zXm(hH+zz;g{pO?gzd^14$h!VF@b!O5-#^LOzcJZ=4*>taegB#6@6P`)_z%VZQ~noN gwD+XK>6ieoi^@0snosr5I4eL$%kXizrv0n`0R>9jX#fBK diff --git a/README.md b/README.md index 475f88fd..b1cfba40 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ ![Parse Server logo](.github/parse-server-logo.png?raw=true) -## Open Source Parse API Server - [![Build Status](https://img.shields.io/travis/ParsePlatform/parse-server/master.svg?style=flat)](https://travis-ci.org/ParsePlatform/parse-server) [![Coverage Status](https://img.shields.io/codecov/c/github/ParsePlatform/parse-server/master.svg)](https://codecov.io/github/ParsePlatform/parse-server?branch=master) [![npm version](https://img.shields.io/npm/v/parse-server.svg?style=flat)](https://www.npmjs.com/package/parse-server) From 61b4468dac7360568f1e292e6e9de46d79f8fb68 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Wed, 17 Feb 2016 19:00:17 -0800 Subject: [PATCH 5/9] Implement DELETE /schemas/:className --- spec/schemas.spec.js | 101 +++++++++++++++++++++++++++++++++++++++++++ src/Schema.js | 3 +- src/schemas.js | 85 ++++++++++++++++++++++++++++++++++++ 3 files changed, 188 insertions(+), 1 deletion(-) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index fd136df4..1a6a3069 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -1,6 +1,9 @@ var Parse = require('parse/node').Parse; var request = require('request'); var dd = require('deep-diff'); +var Config = require('../src/Config'); + +var config = new Config('test'); var hasAllPODobject = () => { var obj = new Parse.Object('HasAllPOD'); @@ -633,4 +636,102 @@ describe('schemas', () => { }); }); }); + + it('requires the master key to delete schemas', done => { + request.del({ + url: 'http://localhost:8378/1/schemas/DoesntMatter', + headers: noAuthHeaders, + json: true, + }, (error, response, body) => { + expect(response.statusCode).toEqual(403); + expect(body.error).toEqual('unauthorized'); + done(); + }); + }); + + it('refuses to delete non-empty collection', done => { + var obj = hasAllPODobject(); + obj.save() + .then(() => { + request.del({ + url: 'http://localhost:8378/1/schemas/HasAllPOD', + headers: masterKeyHeaders, + json: true, + }, (error, response, body) => { + expect(response.statusCode).toEqual(400); + expect(body.code).toEqual(255); + expect(body.error).toEqual('class HasAllPOD not empty, contains 1 objects, cannot drop schema'); + done(); + }); + }); + }); + + it('fails when deleting collections with invalid class names', done => { + request.del({ + url: 'http://localhost:8378/1/schemas/_GlobalConfig', + headers: masterKeyHeaders, + json: true, + }, (error, response, body) => { + expect(response.statusCode).toEqual(400); + expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME); + expect(body.error).toEqual('Invalid classname: _GlobalConfig, classnames can only have alphanumeric characters and _, and must start with an alpha character '); + done(); + }) + }); + + it('does not fail when deleting nonexistant collections', done => { + request.del({ + url: 'http://localhost:8378/1/schemas/Missing', + headers: masterKeyHeaders, + json: true, + }, (error, response, body) => { + expect(response.statusCode).toEqual(200); + expect(body).toEqual({}); + done(); + }); + }); + + it('deletes collections including join tables', done => { + var obj = new Parse.Object('MyClass'); + obj.set('data', 'data'); + obj.save() + .then(() => { + var obj2 = new Parse.Object('MyOtherClass'); + var relation = obj2.relation('aRelation'); + relation.add(obj); + return obj2.save(); + }) + .then(obj2 => obj2.destroy()) + .then(() => { + request.del({ + url: 'http://localhost:8378/1/schemas/MyOtherClass', + headers: masterKeyHeaders, + json: true, + }, (error, response, body) => { + expect(response.statusCode).toEqual(200); + expect(response.body).toEqual({}); + config.database.db.collection('test__Join:aRelation:MyOtherClass', { strict: true }, (err, coll) => { + //Expect Join table to be gone + expect(err).not.toEqual(null); + config.database.db.collection('test_MyOtherClass', { strict: true }, (err, coll) => { + // Expect data table to be gone + expect(err).not.toEqual(null); + request.get({ + url: 'http://localhost:8378/1/schemas/MyOtherClass', + headers: masterKeyHeaders, + json: true, + }, (error, response, body) => { + //Expect _SCHEMA entry to be gone. + expect(response.statusCode).toEqual(400); + expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME); + expect(body.error).toEqual('class MyOtherClass does not exist'); + done(); + }); + }); + }); + }); + }, error => { + fail(error); + }); + }); }); diff --git a/src/Schema.js b/src/Schema.js index a07018bf..0d601449 100644 --- a/src/Schema.js +++ b/src/Schema.js @@ -521,7 +521,7 @@ Schema.prototype.deleteField = function(fieldName, className, database, prefix) }); } - if (schema.data[className][fieldName].startsWith('relation')) { + if (schema.data[className][fieldName].startsWith('relation<')) { //For relations, drop the _Join table return database.dropCollection(prefix + '_Join:' + fieldName + ':' + className) //Save the _SCHEMA object @@ -714,6 +714,7 @@ function getObjectType(obj) { module.exports = { load: load, classNameIsValid: classNameIsValid, + invalidClassNameMessage: invalidClassNameMessage, mongoSchemaFromFieldsAndClassName: mongoSchemaFromFieldsAndClassName, schemaAPITypeToMongoFieldType: schemaAPITypeToMongoFieldType, buildMergedSchemaObject: buildMergedSchemaObject, diff --git a/src/schemas.js b/src/schemas.js index cd8b92ec..9fb191c8 100644 --- a/src/schemas.js +++ b/src/schemas.js @@ -183,10 +183,95 @@ function modifySchema(req) { }); } +// A helper function that removes all join tables for a schema. Returns a promise. +var removeJoinTables = (database, prefix, mongoSchema) => { + return Promise.all(Object.keys(mongoSchema) + .filter(field => mongoSchema[field].startsWith('relation<')) + .map(field => { + var joinCollectionName = prefix + '_Join:' + field + ':' + mongoSchema._id; + return new Promise((resolve, reject) => { + database.dropCollection(joinCollectionName, (err, results) => { + if (err) { + reject(err); + } else { + resolve(); + } + }) + }); + }) + ); +}; + +function deleteSchema(req) { + if (!req.auth.isMaster) { + return masterKeyRequiredResponse(); + } + + if (!Schema.classNameIsValid(req.params.className)) { + return Promise.resolve({ + status: 400, + response: { + code: Parse.Error.INVALID_CLASS_NAME, + error: Schema.invalidClassNameMessage(req.params.className), + } + }); + } + + return req.config.database.collection(req.params.className) + .then(coll => new Promise((resolve, reject) => { + coll.count((err, count) => { + if (err) { + reject(err); + } else if (count > 0) { + resolve({ + status: 400, + response: { + code: 255, + error: 'class ' + req.params.className + ' not empty, contains ' + count + ' objects, cannot drop schema', + } + }); + } else { + coll.drop((err, reply) => { + if (err) { + reject(err); + } else { + // We've dropped the collection now, so delete the item from _SCHEMA + // and clear the _Join collections + req.config.database.collection('_SCHEMA') + .then(coll => new Promise((resolve, reject) => { + coll.findAndRemove({ _id: req.params.className }, [], (err, doc) => { + if (err) { + reject(err); + } else if (doc.value === null) { + //tried to delete non-existant class + resolve({ response: {}}); + } else { + removeJoinTables(req.config.database.db, req.config.database.collectionPrefix, doc.value) + .then(resolve, reject); + } + }); + })) + .then(resolve.bind(undefined, {response: {}}), reject); + } + }); + } + }); + })) + .catch(error => { + if (error.message == 'ns not found') { + // If they try to delete a non-existant class, thats fine, just let them. + return Promise.resolve({ response: {} }); + } else { + return Promise.reject(error); + } + }); +} + router.route('GET', '/schemas', getAllSchemas); router.route('GET', '/schemas/:className', getOneSchema); router.route('POST', '/schemas', createSchema); router.route('POST', '/schemas/:className', createSchema); router.route('PUT', '/schemas/:className', modifySchema); +router.route('DELETE', '/schemas/:className', deleteSchema); module.exports = router; From f271ac9ac10e9873fdec2c14a84499297572b362 Mon Sep 17 00:00:00 2001 From: jim1_lin Date: Thu, 18 Feb 2016 15:36:21 +0800 Subject: [PATCH 6/9] ExportAdapter destroy nothing will cause change password failed Changing password without any session will failed, due to clear no session with response ObjectNotFound. --- src/ExportAdapter.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ExportAdapter.js b/src/ExportAdapter.js index 6587df79..abcc862d 100644 --- a/src/ExportAdapter.js +++ b/src/ExportAdapter.js @@ -306,7 +306,8 @@ ExportAdapter.prototype.destroy = function(className, query, options = {}) { return coll.remove(mongoWhere); }).then((resp) => { - if (resp.result.n === 0) { + //Check _Session to avoid changing password failed without any session. + if (resp.result.n === 0 && className !== "_Session") { return Promise.reject( new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.')); From cc55bfb7ba3d5ab768f58acda31815c9ebf381ef Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Thu, 18 Feb 2016 10:54:53 -0500 Subject: [PATCH 7/9] Fix missing session token when fetching a _User --- src/Routers/ClassesRouter.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js index c9fe9c48..95a27ef1 100644 --- a/src/Routers/ClassesRouter.js +++ b/src/Routers/ClassesRouter.js @@ -54,10 +54,17 @@ export class ClassesRouter { throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.'); } - if(req.params.className === "_User"){ + if (req.params.className === "_User") { + delete response.results[0].sessionToken; - } - + + const user = response.results[0]; + + if (req.auth.user && user.objectId == req.auth.user.id) { + // Force the session token + response.results[0].sessionToken = req.info.sessionToken; + } + } return { response: response.results[0] }; }); } From d42b08055b8e8d8fd60dcbb23ac9338a8bf8cbb7 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Thu, 18 Feb 2016 08:05:08 -0500 Subject: [PATCH 8/9] Moved the proper facebook auth data validation --- src/facebook.js | 58 ------------------------------------------- src/oauth/facebook.js | 3 ++- 2 files changed, 2 insertions(+), 59 deletions(-) delete mode 100644 src/facebook.js diff --git a/src/facebook.js b/src/facebook.js deleted file mode 100644 index 77e0e213..00000000 --- a/src/facebook.js +++ /dev/null @@ -1,58 +0,0 @@ -// Helper functions for accessing the Facebook Graph API. -var https = require('https'); -var Parse = require('parse/node').Parse; - -// Returns a promise that fulfills iff this user id is valid. -function validateAuthData(authData) { - return graphRequest('me?fields=id&access_token=' + authData.access_token) - .then((data) => { - if (data && data.id == authData.id) { - return; - } - throw new Parse.Error( - Parse.Error.OBJECT_NOT_FOUND, - 'Facebook auth is invalid for this user.'); - }); -} - -// Returns a promise that fulfills iff this app id is valid. -function validateAppId(appIds, authData) { - var access_token = authData.access_token; - if (!appIds.length) { - throw new Parse.Error( - Parse.Error.OBJECT_NOT_FOUND, - 'Facebook auth is not configured.'); - } - return graphRequest('app?access_token=' + access_token) - .then((data) => { - if (data && appIds.indexOf(data.id) != -1) { - return; - } - throw new Parse.Error( - Parse.Error.OBJECT_NOT_FOUND, - 'Facebook auth is invalid for this user.'); - }); -} - -// A promisey wrapper for FB graph requests. -function graphRequest(path) { - return new Promise(function(resolve, reject) { - https.get('https://graph.facebook.com/v2.5/' + path, function(res) { - var data = ''; - res.on('data', function(chunk) { - data += chunk; - }); - res.on('end', function() { - data = JSON.parse(data); - resolve(data); - }); - }).on('error', function(e) { - reject('Failed to validate this access token with Facebook.'); - }); - }); -} - -module.exports = { - validateAppId: validateAppId, - validateAuthData: validateAuthData -}; diff --git a/src/oauth/facebook.js b/src/oauth/facebook.js index 822df711..77e0e213 100644 --- a/src/oauth/facebook.js +++ b/src/oauth/facebook.js @@ -16,7 +16,8 @@ function validateAuthData(authData) { } // Returns a promise that fulfills iff this app id is valid. -function validateAppId(appIds, access_token) { +function validateAppId(appIds, authData) { + var access_token = authData.access_token; if (!appIds.length) { throw new Parse.Error( Parse.Error.OBJECT_NOT_FOUND, From 4e453925f7ff21a14acd67aff675da3a1fb07be0 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Thu, 18 Feb 2016 13:49:13 -0500 Subject: [PATCH 9/9] Minor readme change --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index b1cfba40..3ca2801f 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,9 @@ For more informations about custom auth please see the examples: * databaseAdapter (unfinished) - The backing store can be changed by creating an adapter class (see `DatabaseAdapter.js`) * loggerAdapter - The default behavior/transport (File) can be changed by creating an adapter class (see [`LoggerAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Logger/LoggerAdapter.js)) * enableAnonymousUsers - Defaults to true. Set to false to disable anonymous users. + + + --- ### Usage