1
0
Fork 0
mirror of synced 2024-06-02 19:04:49 +12:00

Merge branch 'feat-storage-buckets' into feat-sb-uis

This commit is contained in:
Damodar Lohani 2021-11-25 13:41:12 +05:45
commit e7f7ce0e70
74 changed files with 6261 additions and 1159 deletions

View file

@ -1,24 +1,24 @@
{
"settings.inspire": "\"هنر خردمند بودن، هنر دانستن چیزی است که باید از آن غافل شد.\"",
"settings.inspire": "\"هنر خردمند بودن این است که بدانید چه چیزی را نادیده بگیرید.\"",
"settings.locale": "fa",
"settings.direction": "rtl",
"emails.sender": "تیم %s",
"emails.verification.subject": "تأیید حساب",
"emails.verification.hello": "سلام {{name}}",
"emails.verification.body": "برای تأیید حساب‌تان پیوند زیر را دنبال کنید.",
"emails.verification.footer": "اگر شما درخواست تأیید حساب نداده‌اید، می‌توانید این ایمیل را نادیده بگیرید.",
"emails.verification.body": "برای تأیید ایمیل‌تان پیوند زیر را دنبال کنید.",
"emails.verification.footer": "اگر شما درخواست تأیید حساب نداده‌اید، می‌توانید این پیام را نادیده بگیرید.",
"emails.verification.thanks": "سپاس فراوان",
"emails.verification.signature": "تیم {{name}}",
"emails.magicSession.subject": "ورود به حساب کاربری",
"emails.magicSession.hello": "سلام،",
"emails.magicSession.body": "برای ورود به حساب‌تان پیوند زیر را دنبال کنید.",
"emails.magicSession.footer": "اگر شما درخواست ورود به حساب کاربری نداد‌ه‌اید، می‌توانید این ایمیل را نادیده بگیرید.",
"emails.magicSession.footer": "اگر شما درخواست ورود به حساب کاربری با استفاده از این ایمیل را نداد‌ه‌اید، می‌توانید این پیام را نادیده بگیرید.",
"emails.magicSession.thanks": "سپاس فراوان",
"emails.magicSession.signature": "تیم {{name}}",
"emails.recovery.subject": "بازیابی گذرواژه",
"emails.recovery.hello": "سلام {{name}}",
"emails.recovery.body": "برای بازیابی گذرواژه‌تان پیوند زیر را دنبال کنید.",
"emails.recovery.footer": "اگر شما درخواست بازیابی گذرواژه نداده‌اید، می‌توانید این ایمیل را نادیده بگیرید.",
"emails.recovery.footer": "اگر شما درخواست بازیابی گذرواژه نداده‌اید، می‌توانید این پیام را نادیده بگیرید.",
"emails.recovery.thanks": "سپاس فراوان",
"emails.recovery.signature": "تیم {{name}}",
"emails.invitation.subject": "دعوت به تیم %s در %s",

View file

@ -28,205 +28,205 @@
"emails.invitation.thanks": "Go raibh maith agat",
"emails.invitation.signature": "{{project}} foireann",
"locale.country.unknown": "Neamhaithnid",
"countries.af": "Afghanistan",
"countries.ao": "Angola",
"countries.al": "Albania",
"countries.ad": "Andorra",
"countries.ae": "United Arab Emirates",
"countries.ar": "Argentina",
"countries.am": "Armenia",
"countries.ag": "Antigua and Barbuda",
"countries.au": "Australia",
"countries.at": "Austria",
"countries.az": "Azerbaijan",
"countries.bi": "Burundi",
"countries.be": "Belgium",
"countries.bj": "Benin",
"countries.bf": "Burkina Faso",
"countries.bd": "Bangladesh",
"countries.bg": "Bulgaria",
"countries.bh": "Bahrain",
"countries.bs": "Bahamas",
"countries.ba": "Bosnia and Herzegovina",
"countries.by": "Belarus",
"countries.bz": "Belize",
"countries.bo": "Bolivia",
"countries.br": "Brazil",
"countries.bb": "Barbados",
"countries.bn": "Brunei",
"countries.bt": "Bhutan",
"countries.bw": "Botswana",
"countries.cf": "Central African Republic",
"countries.ca": "Canada",
"countries.ch": "Switzerland",
"countries.cl": "Chile",
"countries.cn": "China",
"countries.ci": "Ivory Coast",
"countries.cm": "Cameroon",
"countries.cd": "DR Congo",
"countries.cg": "Republic of the Congo",
"countries.co": "Colombia",
"countries.km": "Comoros",
"countries.af": "An Afganastáin",
"countries.ao": "Angóla",
"countries.al": "Albáin",
"countries.ad": "Andóra",
"countries.ae": "Aontas na nÉimíríochtaí Arabacha",
"countries.ar": "Airgintín",
"countries.am": "Airméin",
"countries.ag": "Antigua agus Barbúda",
"countries.au": "Astráil",
"countries.at": "Ostair",
"countries.az": "Asarbaiseáin",
"countries.bi": "An Bhurúin",
"countries.be": "An Bheilg",
"countries.bj": "Beinin",
"countries.bf": "Buircíne Fasó",
"countries.bd": "An Bhanglaidéis",
"countries.bg": "An Bhulgáir",
"countries.bh": "Bairéin",
"countries.bs": "Na Bahámaí",
"countries.ba": "An Bhoisnia agus an Heirseagaivéin",
"countries.by": "An Bhealarúis",
"countries.bz": "An Bheilís",
"countries.bo": "An Bholaiv",
"countries.br": "An Bhrasaíl",
"countries.bb": "Barbadós",
"countries.bn": "Brúiné",
"countries.bt": "An Bhútáin",
"countries.bw": "An Bhotsuáin",
"countries.cf": "Poblacht na hAfraice Láir",
"countries.ca": "Ceanada",
"countries.ch": "An Eilvéis",
"countries.cl": "An tSile",
"countries.cn": "An tSín",
"countries.ci": "Poblacht Côte dIvoire",
"countries.cm": "Camarún",
"countries.cd": "Poblacht Dhaonlathach an Chongó",
"countries.cg": "Poblacht an Chongó",
"countries.co": "An Cholóim",
"countries.km": "Oileáin Chomóra",
"countries.cv": "Cape Verde",
"countries.cr": "Costa Rica",
"countries.cu": "Cuba",
"countries.cy": "Cyprus",
"countries.cz": "Czechia",
"countries.de": "Germany",
"countries.cr": "Cósta Ríce",
"countries.cu": "Cúba",
"countries.cy": "An Chipir",
"countries.cz": "An tSeicia",
"countries.de": "An Ghearmáin",
"countries.dj": "Djibouti",
"countries.dm": "Dominica",
"countries.dk": "Denmark",
"countries.do": "Dominican Republic",
"countries.dz": "Algeria",
"countries.ec": "Ecuador",
"countries.eg": "Egypt",
"countries.er": "Eritrea",
"countries.es": "Spain",
"countries.ee": "Estonia",
"countries.et": "Ethiopia",
"countries.fi": "Finland",
"countries.fj": "Fiji",
"countries.fr": "France",
"countries.fm": "Micronesia",
"countries.ga": "Gabon",
"countries.gb": "United Kingdom",
"countries.ge": "Georgia",
"countries.gh": "Ghana",
"countries.gn": "Guinea",
"countries.gm": "Gambia",
"countries.gw": "Guinea-Bissau",
"countries.gq": "Equatorial Guinea",
"countries.gr": "Greece",
"countries.gd": "Grenada",
"countries.gt": "Guatemala",
"countries.gy": "Guyana",
"countries.hn": "Honduras",
"countries.hr": "Croatia",
"countries.ht": "Haiti",
"countries.hu": "Hungary",
"countries.id": "Indonesia",
"countries.dm": "Doiminice",
"countries.dk": "An Danmhairg",
"countries.do": "An Phoblacht Dhoiminiceach",
"countries.dz": "An Ailgéir",
"countries.ec": "Eacuadór",
"countries.eg": "An Éigipt",
"countries.er": "An Eiritré",
"countries.es": "An Spáinn",
"countries.ee": "An Eastóin",
"countries.et": "An Aetóip",
"countries.fi": "An Fhionlainn",
"countries.fj": "Fidsí",
"countries.fr": "An Fhrainc",
"countries.fm": "An Mhicrinéis",
"countries.ga": "An Ghabúin",
"countries.gb": "An Ríocht Aontaithe",
"countries.ge": "An tSeoirsia",
"countries.gh": "Gána",
"countries.gn": "An Ghuine",
"countries.gm": "An Ghaimbia",
"countries.gw": "Guine Bissau",
"countries.gq": "An Ghuine Mheánchiorclach",
"countries.gr": "An Ghréig",
"countries.gd": "Greanáda",
"countries.gt": "Guatamala",
"countries.gy": "An Ghuáin",
"countries.hn": "Hondúras",
"countries.hr": "An Chróit",
"countries.ht": "Háítí",
"countries.hu": "An Ungáir",
"countries.id": "An Indinéis",
"countries.in": "India",
"countries.ie": "Ireland",
"countries.ir": "Iran",
"countries.iq": "Iraq",
"countries.is": "Iceland",
"countries.il": "Israel",
"countries.it": "Italy",
"countries.jm": "Jamaica",
"countries.jo": "Jordan",
"countries.jp": "Japan",
"countries.kz": "Kazakhstan",
"countries.ke": "Kenya",
"countries.kg": "Kyrgyzstan",
"countries.kh": "Cambodia",
"countries.ki": "Kiribati",
"countries.kn": "Saint Kitts and Nevis",
"countries.kr": "South Korea",
"countries.kw": "Kuwait",
"countries.ie": "Éire",
"countries.ir": "An Iaráin",
"countries.iq": "An Iaráic",
"countries.is": "An Íoslainn",
"countries.il": "Iosrael",
"countries.it": "An Iodáil",
"countries.jm": "Iamáice",
"countries.jo": "An Iordáin",
"countries.jp": "An tSeapáin",
"countries.kz": "An Chasacstáin",
"countries.ke": "An Chéinia",
"countries.kg": "An Chirgeastáin",
"countries.kh": "An Chambóid",
"countries.ki": "Ciribeas",
"countries.kn": "San Críostóir-Nimheas",
"countries.kr": "An Chóiré Theas",
"countries.kw": "Cuáit",
"countries.la": "Laos",
"countries.lb": "Lebanon",
"countries.lr": "Liberia",
"countries.ly": "Libya",
"countries.lb": "An Liobáin",
"countries.lr": "An Libéir",
"countries.ly": "An Libia",
"countries.lc": "Saint Lucia",
"countries.li": "Liechtenstein",
"countries.lk": "Sri Lanka",
"countries.ls": "Lesotho",
"countries.lt": "Lithuania",
"countries.lu": "Luxembourg",
"countries.lv": "Latvia",
"countries.ma": "Morocco",
"countries.mc": "Monaco",
"countries.md": "Moldova",
"countries.li": "Lichtinstéin",
"countries.lk": "Srí Lanca",
"countries.ls": "Leosóta",
"countries.lt": "An Liotuáin",
"countries.lu": "Lucsamburg",
"countries.lv": "An Laitvia",
"countries.ma": "Maracó",
"countries.mc": "Monacó",
"countries.md": "An Mholdóiv",
"countries.mg": "Madagascar",
"countries.mv": "Maldives",
"countries.mx": "Mexico",
"countries.mh": "Marshall Islands",
"countries.mk": "Macedonia",
"countries.ml": "Mali",
"countries.mt": "Malta",
"countries.mm": "Myanmar",
"countries.me": "Montenegro",
"countries.mn": "Mongolia",
"countries.mz": "Mozambique",
"countries.mr": "Mauritania",
"countries.mu": "Mauritius",
"countries.mw": "Malawi",
"countries.my": "Malaysia",
"countries.na": "Namibia",
"countries.mv": "Oileáin Mhaildíve",
"countries.mx": "Meicsiceo",
"countries.mh": "Oileáin Marshall",
"countries.mk": "An Mhacadóin",
"countries.ml": "Mailí",
"countries.mt": "Málta",
"countries.mm": "Maenmar",
"countries.me": "Montainéagró",
"countries.mn": "An Mhongóil",
"countries.mz": "Mósaimbíc",
"countries.mr": "An Mháratáin",
"countries.mu": "Oileán Mhuirís",
"countries.mw": "Malávach",
"countries.my": "An Mhalaeisia",
"countries.na": "An Namaib",
"countries.ne": "Niger",
"countries.ng": "Nigeria",
"countries.ni": "Nicaragua",
"countries.nl": "Netherlands",
"countries.no": "Norway",
"countries.np": "Nepal",
"countries.nr": "Nauru",
"countries.nz": "New Zealand",
"countries.om": "Oman",
"countries.pk": "Pakistan",
"countries.ng": "An Nigéir",
"countries.ni": "Nicearagua",
"countries.nl": "An Ísiltír",
"countries.no": "An Iorua",
"countries.np": "Neipeal",
"countries.nr": "Nárú",
"countries.nz": "An Nua-Shéalainn",
"countries.om": "Óman",
"countries.pk": "An Phacastáin",
"countries.pa": "Panama",
"countries.pe": "Peru",
"countries.ph": "Philippines",
"countries.pw": "Palau",
"countries.pg": "Papua New Guinea",
"countries.pl": "Poland",
"countries.kp": "North Korea",
"countries.pt": "Portugal",
"countries.py": "Paraguay",
"countries.qa": "Qatar",
"countries.ro": "Romania",
"countries.ru": "Russia",
"countries.rw": "Rwanda",
"countries.sa": "Saudi Arabia",
"countries.sd": "Sudan",
"countries.sn": "Senegal",
"countries.sg": "Singapore",
"countries.sb": "Solomon Islands",
"countries.sl": "Sierra Leone",
"countries.sv": "El Salvador",
"countries.sm": "San Marino",
"countries.so": "Somalia",
"countries.rs": "Serbia",
"countries.ss": "South Sudan",
"countries.st": "São Tomé and Príncipe",
"countries.sr": "Suriname",
"countries.sk": "Slovakia",
"countries.si": "Slovenia",
"countries.se": "Sweden",
"countries.sz": "Swaziland",
"countries.sc": "Seychelles",
"countries.sy": "Syria",
"countries.td": "Chad",
"countries.tg": "Togo",
"countries.th": "Thailand",
"countries.tj": "Tajikistan",
"countries.tm": "Turkmenistan",
"countries.tl": "Timor-Leste",
"countries.pe": "Peiriú",
"countries.ph": "Na hOileáin Fhilipíneacha",
"countries.pw": "Oileáin Palau",
"countries.pg": "Nua-Ghuine Phapua",
"countries.pl": "An Pholainn",
"countries.kp": "An Chóiré Thuaidh",
"countries.pt": "An Phortaingéil",
"countries.py": "Paragua",
"countries.qa": "Catar",
"countries.ro": "An Rómáin",
"countries.ru": "An Rúis",
"countries.rw": "Ruanda",
"countries.sa": "An Araib Shádach",
"countries.sd": "An tSúdáin",
"countries.sn": "An tSeineagáil",
"countries.sg": "Singeapór",
"countries.sb": "Oileáin Sholaimh",
"countries.sl": "Siarra Leon",
"countries.sv": "An tSalvadóir",
"countries.sm": "San Mairíne",
"countries.so": "An tSomáil",
"countries.rs": "An tSeirbia",
"countries.ss": "An tSúdáin Theas",
"countries.st": "São Tomé agus Príncipe",
"countries.sr": "Suranam",
"countries.sk": "An tSlóvaic",
"countries.si": "An tSlóivéin",
"countries.se": "An tSualainn",
"countries.sz": "An tSuasalainn",
"countries.sc": "Na Séiséil",
"countries.sy": "An tSiria",
"countries.td": "Sead",
"countries.tg": "Tóga",
"countries.th": "An Téalainn",
"countries.tj": "An Táidsíceastáin",
"countries.tm": "An Tuircméanastáin",
"countries.tl": "Tíomór Thoir",
"countries.to": "Tonga",
"countries.tt": "Trinidad and Tobago",
"countries.tn": "Tunisia",
"countries.tr": "Turkey",
"countries.tv": "Tuvalu",
"countries.tz": "Tanzania",
"countries.tt": "Oileán na Tríonóide agus Tobága",
"countries.tn": "An Túinéis",
"countries.tr": "An Tuirc",
"countries.tv": "Túvalú",
"countries.tz": "An Tansáin",
"countries.ug": "Uganda",
"countries.ua": "Ukraine",
"countries.uy": "Uruguay",
"countries.us": "United States",
"countries.uz": "Uzbekistan",
"countries.va": "Vatican City",
"countries.vc": "Saint Vincent and the Grenadines",
"countries.ve": "Venezuela",
"countries.vn": "Vietnam",
"countries.vu": "Vanuatu",
"countries.ws": "Samoa",
"countries.ye": "Yemen",
"countries.za": "South Africa",
"countries.zm": "Zambia",
"countries.zw": "Zimbabwe",
"continents.af": "Africa",
"continents.an": "Antarctica",
"continents.as": "Asia",
"continents.eu": "Europe",
"continents.na": "North America",
"continents.oc": "Oceania",
"continents.sa": "South America"
"countries.ua": "An Úcráin",
"countries.uy": "Uragua",
"countries.us": "Na Stáit Aontaithe",
"countries.uz": "An Úisbéiceastáin",
"countries.va": "Cathair na Vatacáine",
"countries.vc": "San Uinseann agus na Greanáidíní",
"countries.ve": "Veiniséala",
"countries.vn": "Vítneam",
"countries.vu": "Vanuatú",
"countries.ws": "Samó",
"countries.ye": "Éimin",
"countries.za": "An Afraic Theas",
"countries.zm": "An tSaimbia",
"countries.zw": "An tSiombáib",
"continents.af": "An Afraic",
"continents.an": "Antartaice",
"continents.as": "An Áise",
"continents.eu": "An Eoraip",
"continents.na": "Meiriceá Thuaidh",
"continents.oc": "An Aigéine",
"continents.sa": "Meiriceá Theas"
}

View file

@ -3,30 +3,30 @@
"settings.locale": "jv",
"settings.direction": "ltr",
"emails.sender": "Tim %s",
"emails.verification.subject": "",
"emails.verification.hello": "",
"emails.verification.body": "",
"emails.verification.footer": "",
"emails.verification.thanks": "",
"emails.verification.signature": "",
"emails.magicSession.subject": "",
"emails.magicSession.hello": "",
"emails.magicSession.body": "",
"emails.magicSession.footer": "",
"emails.magicSession.thanks": "",
"emails.magicSession.signature": "",
"emails.recovery.subject": "",
"emails.recovery.hello": "",
"emails.recovery.body": "",
"emails.recovery.footer": "",
"emails.recovery.thanks": "",
"emails.recovery.signature": "",
"emails.invitation.subject": "",
"emails.invitation.hello": "",
"emails.invitation.body": "",
"emails.invitation.footer": "",
"emails.invitation.thanks": "",
"emails.invitation.signature": "",
"emails.verification.subject": "Verifikasi Akun",
"emails.verification.hello": "Hai {{name}}",
"emails.verification.body": "Klik link iki kanggo verifikasi alamat email sampeyan.",
"emails.verification.footer": "Yen sampeyan ora njaluk verifikasi alamat iki, sampeyan iso nglirwakake pesen iki.",
"emails.verification.thanks": "Matur nuwun",
"emails.verification.signature": "Tim {{project}}",
"emails.magicSession.subject": "Masuk",
"emails.magicSession.hello": "Hai,",
"emails.magicSession.body": "Klik link iki kanggo masuk.",
"emails.magicSession.footer": "Yen sampeyan ora njaluk masuk nggunakake alamat email iki, sampeyan iso nglirwakake pesen iki.",
"emails.magicSession.thanks": "Matur nuwun",
"emails.magicSession.signature": "Tim {{project}}",
"emails.recovery.subject": "Setel ulang sandi",
"emails.recovery.hello": "Halo {{name}}",
"emails.recovery.body": "Klik link iki kanggo setel ulang sandi {{project}}.",
"emails.recovery.footer": "Yen sampeyan ora njaluk setel ulang sandi, sampeyan iso nglirwakake pesen iki.",
"emails.recovery.thanks": "Matur nuwun",
"emails.recovery.signature": "Tim {{project}}",
"emails.invitation.subject": "Undangan ke Tim %s di %s",
"emails.invitation.hello": "Halo",
"emails.invitation.body": "Email iki dikirim menyang sampeyan amarga {{owner}} pengin ngajak sampeyan dadi anggota tim {{team}} di {{project}}.",
"emails.invitation.footer": "Yen sampeyan ora tertarik, sampeyan iso nglirwakake pesen iki.",
"emails.invitation.thanks": "Matur nuwun",
"emails.invitation.signature": "Tim {{project}}",
"locale.country.unknown": "Ora dingerteni",
"countries.af": "Afghanistan",
"countries.ao": "Angola",

View file

@ -3,30 +3,30 @@
"settings.locale": "mr",
"settings.direction": "ltr",
"emails.sender": "%s टीम",
"emails.verification.subject": "",
"emails.verification.hello": "",
"emails.verification.body": "",
"emails.verification.footer": "",
"emails.verification.thanks": "",
"emails.verification.signature": "",
"emails.magicSession.subject": "",
"emails.magicSession.hello": "",
"emails.magicSession.body": "",
"emails.magicSession.footer": "",
"emails.magicSession.thanks": "",
"emails.magicSession.signature": "",
"emails.recovery.subject": "",
"emails.recovery.hello": "",
"emails.recovery.body": "",
"emails.recovery.footer": "",
"emails.recovery.thanks": "",
"emails.recovery.signature": "",
"emails.invitation.subject": "",
"emails.invitation.hello": "",
"emails.invitation.body": "",
"emails.invitation.footer": "",
"emails.invitation.thanks": "",
"emails.invitation.signature": "",
"emails.verification.subject": "खाते सत्यापन",
"emails.verification.hello": "नमस्कार {{name}}",
"emails.verification.body": "आपला ईमेल पत्ता सत्यापित करण्यासाठी या दुव्याचे अनुसरण करा.",
"emails.verification.footer": "आपण या पत्त्याची पडताळणी करण्यास सांगितले नसल्यास, आपण या संदेशाकडे दुर्लक्ष करू शकता.",
"emails.verification.thanks": "धन्यवाद",
"emails.verification.signature": "{{project}} संघ",
"emails.magicSession.subject": "लॉगिन करा",
"emails.magicSession.hello": "नमस्कार ,",
"emails.magicSession.body": "लॉगिन करण्यासाठी या लिंकचे अनुसरण करा.",
"emails.magicSession.footer": "आपण या ईमेलचा वापर करून लॉगिन करण्यास सांगितले नसल्यास, आपण या संदेशाकडे दुर्लक्ष करू शकता.",
"emails.magicSession.thanks": "धन्यवाद",
"emails.magicSession.signature": "{{project}} संघ",
"emails.recovery.subject": "पासवर्ड रीसेट",
"emails.recovery.hello": "नमस्कार {{name}}",
"emails.recovery.body": "आपला {{project}}चे पासवर्ड रीसेट करण्यासाठी या लिंकचे अनुसरण करा",
"emails.recovery.footer": "आपण आपला पासवर्ड रीसेट करण्यास सांगितले नसल्यास, आपण या संदेशाकडे दुर्लक्ष करू शकता.",
"emails.recovery.thanks": "धन्यवाद",
"emails.recovery.signature": "{{project}} संघ",
"emails.invitation.subject": "%s संघ %s येथे सामील होण्यासाठी आमंत्रण",
"emails.invitation.hello": "नमस्कार",
"emails.invitation.body": "हा मेल तुम्हाला पाठवला होता कारण {{owner}} तुम्हाला {{project}} येथे {{team}} टीमचे सदस्य होण्यासाठी आमंत्रित करू इच्छित होते.",
"emails.invitation.footer": "आपल्याला स्वारस्य नसल्यास, आपण या संदेशाकडे दुर्लक्ष करू शकता.",
"emails.invitation.thanks": "धन्यवाद",
"emails.invitation.signature": "{{project}} संघ",
"locale.country.unknown": "अज्ञात",
"countries.af": "अफगानिस्तान",
"countries.ao": "अंगोला",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -23,6 +23,7 @@ use Utopia\Database\Validator\UID;
use Utopia\Exception;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@ -217,6 +218,8 @@ App::post('/v1/account/sessions')
->setParam('userId', $profile->getId())
->setParam('event', 'account.sessions.create')
->setParam('resource', 'user/' . $profile->getId())
->setParam('userEmail', $profile->getAttribute('email', ''))
->setParam('userName', $profile->getAttribute('name', ''))
;
if (!Config::getParam('domainVerification')) {
@ -1182,13 +1185,15 @@ App::get('/v1/account/logs')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_LOG_LIST)
->param('limit', 25, new Range(0, 100), 'Maximum number of logs to return in response. Use this value to manage pagination. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, 900000000), 'Offset value. The default value is 0. Use this param to manage pagination.', true)
->inject('response')
->inject('user')
->inject('locale')
->inject('geodb')
->inject('dbForInternal')
->inject('usage')
->action(function ($response, $user, $locale, $geodb, $dbForInternal, $usage) {
->action(function ($limit, $offset, $response, $user, $locale, $geodb, $dbForInternal, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Document $user */
@ -1198,8 +1203,7 @@ App::get('/v1/account/logs')
/** @var Appwrite\Stats\Stats $usage */
$audit = new Audit($dbForInternal);
$logs = $audit->getLogsByUserAndEvents($user->getId(), [
$auditEvents = [
'account.create',
'account.delete',
'account.update.name',
@ -1215,7 +1219,9 @@ App::get('/v1/account/logs')
'teams.membership.create',
'teams.membership.update',
'teams.membership.delete',
]);
];
$logs = $audit->getLogsByUserAndEvents($user->getId(), $auditEvents, $limit, $offset);
$output = [];
@ -1224,17 +1230,19 @@ App::get('/v1/account/logs')
$detector = new Detector($log['userAgent']);
$output[$i] = new Document(array_merge([
'event' => $log['event'],
'ip' => $log['ip'],
'time' => $log['time'],
], $detector->getOS(), $detector->getClient(), $detector->getDevice()));
$output[$i] = new Document(array_merge(
$log->getArrayCopy(),
$log['data'],
$detector->getOS(),
$detector->getClient(),
$detector->getDevice()
));
$record = $geodb->get($log['ip']);
if ($record) {
$output[$i]['countryCode'] = $locale->getText('countries.'.strtolower($record['country']['iso_code']), false) ? \strtolower($record['country']['iso_code']) : '--';
$output[$i]['countryName'] = $locale->getText('countries.'.strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown'));
$output[$i]['countryName'] = $locale->getText('countries.'.strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown'));
} else {
$output[$i]['countryCode'] = '--';
$output[$i]['countryName'] = $locale->getText('locale.country.unknown');
@ -1245,7 +1253,11 @@ App::get('/v1/account/logs')
$usage
->setParam('users.read', 1)
;
$response->dynamic(new Document(['logs' => $output]), Response::MODEL_LOG_LIST);
$response->dynamic(new Document([
'sum' => $audit->countLogsByUserAndEvents($user->getId(), $auditEvents),
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
App::get('/v1/account/sessions/:sessionId')

View file

@ -40,13 +40,14 @@ use DeviceDetector\DeviceDetector;
* @param Utopia\Database\Document $attribute
* @param Appwrite\Utopia\Response $response
* @param Utopia\Database\Database $dbForInternal
* @param Utopia\Database\Database $dbForExternal
* @param Appwrite\Event\Event $database
* @param Appwrite\Event\Event $audits
* @param Appwrite\Stats\Stats $usage
*
* @return Document Newly created attribute document
*/
function createAttribute($collectionId, $attribute, $response, $dbForInternal, $database, $audits, $usage): Document
function createAttribute($collectionId, $attribute, $response, $dbForInternal, $dbForExternal, $database, $audits, $usage): Document
{
$attributeId = $attribute->getId();
$type = $attribute->getAttribute('type', '');
@ -94,6 +95,7 @@ function createAttribute($collectionId, $attribute, $response, $dbForInternal, $
'array' => $array,
'format' => $format,
'formatOptions' => $formatOptions,
'filters' => $filters,
]);
$dbForInternal->checkAttribute($collection, $attribute);
@ -107,6 +109,7 @@ function createAttribute($collectionId, $attribute, $response, $dbForInternal, $
}
$dbForInternal->deleteCachedDocument('collections', $collectionId);
$dbForExternal->deleteCachedCollection($collectionId);
// Pass clone of $attribute object to workers
// so we can later modify Document to fit response model
@ -287,7 +290,7 @@ App::get('/v1/database/usage')
$usage = [];
if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') {
$period = [
$periods = [
'24h' => [
'period' => '30m',
'limit' => 48,
@ -321,13 +324,16 @@ App::get('/v1/database/usage')
$stats = [];
Authorization::skip(function() use ($dbForInternal, $period, $range, $metrics, &$stats) {
Authorization::skip(function() use ($dbForInternal, $periods, $range, $metrics, &$stats) {
foreach ($metrics as $metric) {
$limit = $periods[$range]['limit'];
$period = $periods[$range]['period'];
$requestDocs = $dbForInternal->find('stats', [
new Query('period', Query::TYPE_EQUAL, [$period[$range]['period']]),
new Query('period', Query::TYPE_EQUAL, [$period]),
new Query('metric', Query::TYPE_EQUAL, [$metric]),
], $period[$range]['limit'], 0, ['time'], [Database::ORDER_DESC]);
], $limit, 0, ['time'], [Database::ORDER_DESC]);
$stats[$metric] = [];
foreach ($requestDocs as $requestDoc) {
$stats[$metric][] = [
@ -335,22 +341,38 @@ App::get('/v1/database/usage')
'date' => $requestDoc->getAttribute('time'),
];
}
// backfill metrics with empty values for graphs
$backfill = $limit - \count($requestDocs);
while ($backfill > 0) {
$last = $limit - $backfill - 1; // array index of last added metric
$diff = match($period) { // convert period to seconds for unix timestamp math
'30m' => 1800,
'1d' => 86400,
};
$stats[$metric][] = [
'value' => 0,
'date' => ($stats[$metric][$last]['date'] ?? \time()) - $diff, // time of last metric minus period
];
$backfill--;
}
// TODO@kodumbeats explore performance if query is ordered by time ASC
$stats[$metric] = array_reverse($stats[$metric]);
}
}
});
$usage = new Document([
'range' => $range,
'documents.count' => $stats["database.documents.count"],
'collections.count' => $stats["database.collections.count"],
'documents.create' => $stats["database.documents.create"],
'documents.read' => $stats["database.documents.read"],
'documents.update' => $stats["database.documents.update"],
'documents.delete' => $stats["database.documents.delete"],
'collections.create' => $stats["database.collections.create"],
'collections.read' => $stats["database.collections.read"],
'collections.update' => $stats["database.collections.update"],
'collections.delete' => $stats["database.collections.delete"],
'documentsCount' => $stats["database.documents.count"],
'collectionsCount' => $stats["database.collections.count"],
'documentsCreate' => $stats["database.documents.create"],
'documentsRead' => $stats["database.documents.read"],
'documentsUpdate' => $stats["database.documents.update"],
'documentsDelete' => $stats["database.documents.delete"],
'collectionsCreate' => $stats["database.collections.create"],
'collectionsRead' => $stats["database.collections.read"],
'collectionsUpdate' => $stats["database.collections.update"],
'collectionsDelete' => $stats["database.collections.delete"],
]);
}
@ -386,7 +408,7 @@ App::get('/v1/database/:collectionId/usage')
$usage = [];
if(App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') {
$period = [
$periods = [
'24h' => [
'period' => '30m',
'limit' => 48,
@ -415,13 +437,16 @@ App::get('/v1/database/:collectionId/usage')
$stats = [];
Authorization::skip(function() use ($dbForInternal, $period, $range, $metrics, &$stats) {
Authorization::skip(function() use ($dbForInternal, $periods, $range, $metrics, &$stats) {
foreach ($metrics as $metric) {
$limit = $periods[$range]['limit'];
$period = $periods[$range]['period'];
$requestDocs = $dbForInternal->find('stats', [
new Query('period', Query::TYPE_EQUAL, [$period[$range]['period']]),
new Query('period', Query::TYPE_EQUAL, [$period]),
new Query('metric', Query::TYPE_EQUAL, [$metric]),
], $period[$range]['limit'], 0, ['time'], [Database::ORDER_DESC]);
], $limit, 0, ['time'], [Database::ORDER_DESC]);
$stats[$metric] = [];
foreach ($requestDocs as $requestDoc) {
$stats[$metric][] = [
@ -429,17 +454,32 @@ App::get('/v1/database/:collectionId/usage')
'date' => $requestDoc->getAttribute('time'),
];
}
// backfill metrics with empty values for graphs
$backfill = $limit - \count($requestDocs);
while ($backfill > 0) {
$last = $limit - $backfill - 1; // array index of last added metric
$diff = match($period) { // convert period to seconds for unix timestamp math
'30m' => 1800,
'1d' => 86400,
};
$stats[$metric][] = [
'value' => 0,
'date' => ($stats[$metric][$last]['date'] ?? \time()) - $diff, // time of last metric minus period
];
$backfill--;
}
$stats[$metric] = array_reverse($stats[$metric]);
}
});
$usage = new Document([
'range' => $range,
'documents.count' => $stats["database.collections.$collectionId.documents.count"],
'documents.create' => $stats["database.collections.$collectionId.documents.create"],
'documents.read' => $stats["database.collections.$collectionId.documents.read"],
'documents.update' => $stats["database.collections.$collectionId.documents.update"],
'documents.delete' => $stats["database.collections.$collectionId.documents.delete"]
'documentsCount' => $stats["database.collections.$collectionId.documents.count"],
'documentsCreate' => $stats["database.collections.$collectionId.documents.create"],
'documentsRead' => $stats["database.collections.$collectionId.documents.read"],
'documentsUpdate' => $stats["database.collections.$collectionId.documents.update"],
'documentsDelete' => $stats["database.collections.$collectionId.documents.delete"]
]);
}
@ -458,12 +498,14 @@ App::get('/v1/database/collections/:collectionId/logs')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_LOG_LIST)
->param('collectionId', '', new UID(), 'Collection unique ID.')
->param('limit', 25, new Range(0, 100), 'Maximum number of logs to return in response. Use this value to manage pagination. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, 900000000), 'Offset value. The default value is 0. Use this param to manage pagination.', true)
->inject('response')
->inject('dbForInternal')
->inject('dbForExternal')
->inject('locale')
->inject('geodb')
->action(function ($collectionId, $response, $dbForInternal, $dbForExternal, $locale, $geodb) {
->action(function ($collectionId, $limit, $offset, $response, $dbForInternal, $dbForExternal, $locale, $geodb) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Database $dbForInternal */
@ -478,8 +520,8 @@ App::get('/v1/database/collections/:collectionId/logs')
}
$audit = new Audit($dbForInternal);
$logs = $audit->getLogsByResource('collection/'.$collection->getId());
$resource = 'collection/'.$collection->getId();
$logs = $audit->getLogsByResource($resource, $limit, $offset);
$output = [];
@ -539,7 +581,10 @@ App::get('/v1/database/collections/:collectionId/logs')
}
}
$response->dynamic(new Document(['logs' => $output]), Response::MODEL_LOG_LIST);
$response->dynamic(new Document([
'sum' => $audit->countLogsByResource($resource),
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
App::put('/v1/database/collections/:collectionId')
@ -643,6 +688,8 @@ App::delete('/v1/database/collections/:collectionId')
throw new Exception('Failed to remove collection from DB', 500);
}
$dbForExternal->deleteCachedCollection($collection->getId());
$deletes
->setParam('type', DELETE_TYPE_DOCUMENT)
->setParam('document', $collection)
@ -683,12 +730,14 @@ App::post('/v1/database/collections/:collectionId/attributes/string')
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForInternal')
->inject('dbForExternal')
->inject('database')
->inject('audits')
->inject('usage')
->action(function ($collectionId, $attributeId, $size, $required, $default, $array, $response, $dbForInternal, $database, $audits, $usage) {
->action(function ($collectionId, $attributeId, $size, $required, $default, $array, $response, $dbForInternal, $dbForExternal, $database, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal*/
/** @var Utopia\Database\Database $dbForExternal*/
/** @var Appwrite\Event\Event $database */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
@ -706,7 +755,7 @@ App::post('/v1/database/collections/:collectionId/attributes/string')
'required' => $required,
'default' => $default,
'array' => $array,
]), $response, $dbForInternal, $database, $audits, $usage);
]), $response, $dbForInternal, $dbForExternal, $database, $audits, $usage);
$response->dynamic($attribute, Response::MODEL_ATTRIBUTE_STRING);
});
@ -730,12 +779,14 @@ App::post('/v1/database/collections/:collectionId/attributes/email')
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForInternal')
->inject('dbForExternal')
->inject('database')
->inject('audits')
->inject('usage')
->action(function ($collectionId, $attributeId, $required, $default, $array, $response, $dbForInternal, $database, $audits, $usage) {
->action(function ($collectionId, $attributeId, $required, $default, $array, $response, $dbForInternal, $dbForExternal, $database, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal*/
/** @var Utopia\Database\Database $dbForExternal*/
/** @var Appwrite\Event\Event $database */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
@ -748,7 +799,7 @@ App::post('/v1/database/collections/:collectionId/attributes/email')
'default' => $default,
'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_EMAIL,
]), $response, $dbForInternal, $database, $audits, $usage);
]), $response, $dbForInternal, $dbForExternal, $database, $audits, $usage);
$response->dynamic($attribute, Response::MODEL_ATTRIBUTE_EMAIL);
});
@ -773,12 +824,14 @@ App::post('/v1/database/collections/:collectionId/attributes/enum')
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForInternal')
->inject('dbForExternal')
->inject('database')
->inject('audits')
->inject('usage')
->action(function ($collectionId, $attributeId, $elements, $required, $default, $array, $response, $dbForInternal, $database, $audits, $usage) {
->action(function ($collectionId, $attributeId, $elements, $required, $default, $array, $response, $dbForInternal, $dbForExternal, $database, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal*/
/** @var Utopia\Database\Database $dbForExternal*/
/** @var Appwrite\Event\Event $database */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
@ -803,7 +856,7 @@ App::post('/v1/database/collections/:collectionId/attributes/enum')
'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_ENUM,
'formatOptions' => ['elements' => $elements],
]), $response, $dbForInternal, $database, $audits, $usage);
]), $response, $dbForInternal, $dbForExternal, $database, $audits, $usage);
$response->dynamic($attribute, Response::MODEL_ATTRIBUTE_ENUM);
});
@ -827,12 +880,14 @@ App::post('/v1/database/collections/:collectionId/attributes/ip')
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForInternal')
->inject('dbForExternal')
->inject('database')
->inject('audits')
->inject('usage')
->action(function ($collectionId, $attributeId, $required, $default, $array, $response, $dbForInternal, $database, $audits, $usage) {
->action(function ($collectionId, $attributeId, $required, $default, $array, $response, $dbForInternal, $dbForExternal, $database, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal*/
/** @var Utopia\Database\Database $dbForExternal*/
/** @var Appwrite\Event\Event $database */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
@ -845,7 +900,7 @@ App::post('/v1/database/collections/:collectionId/attributes/ip')
'default' => $default,
'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_IP,
]), $response, $dbForInternal, $database, $audits, $usage);
]), $response, $dbForInternal, $dbForExternal, $database, $audits, $usage);
$response->dynamic($attribute, Response::MODEL_ATTRIBUTE_IP);
});
@ -869,10 +924,11 @@ App::post('/v1/database/collections/:collectionId/attributes/url')
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForInternal')
->inject('dbForExternal')
->inject('database')
->inject('audits')
->inject('usage')
->action(function ($collectionId, $attributeId, $required, $default, $array, $response, $dbForInternal, $database, $audits, $usage) {
->action(function ($collectionId, $attributeId, $required, $default, $array, $response, $dbForInternal, $dbForExternal, $database, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForExternal*/
/** @var Appwrite\Event\Event $database */
@ -887,7 +943,7 @@ App::post('/v1/database/collections/:collectionId/attributes/url')
'default' => $default,
'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_URL,
]), $response, $dbForInternal, $database, $audits, $usage);
]), $response, $dbForInternal, $dbForExternal, $database, $audits, $usage);
$response->dynamic($attribute, Response::MODEL_ATTRIBUTE_URL);
});
@ -913,12 +969,14 @@ App::post('/v1/database/collections/:collectionId/attributes/integer')
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForInternal')
->inject('dbForExternal')
->inject('database')
->inject('audits')
->inject('usage')
->action(function ($collectionId, $attributeId, $required, $min, $max, $default, $array, $response, $dbForInternal, $database, $audits, $usage) {
->action(function ($collectionId, $attributeId, $required, $min, $max, $default, $array, $response, $dbForInternal, $dbForExternal, $database, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal*/
/** @var Utopia\Database\Database $dbForExternal*/
/** @var Appwrite\Event\Event $database */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
@ -949,7 +1007,7 @@ App::post('/v1/database/collections/:collectionId/attributes/integer')
'min' => $min,
'max' => $max,
],
]), $response, $dbForInternal, $database, $audits, $usage);
]), $response, $dbForInternal, $dbForExternal, $database, $audits, $usage);
$formatOptions = $attribute->getAttribute('formatOptions', []);
@ -982,18 +1040,20 @@ App::post('/v1/database/collections/:collectionId/attributes/float')
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForInternal')
->inject('dbForExternal')
->inject('database')
->inject('audits')
->inject('usage')
->action(function ($collectionId, $attributeId, $required, $min, $max, $default, $array, $response, $dbForInternal, $database, $audits, $usage) {
->action(function ($collectionId, $attributeId, $required, $min, $max, $default, $array, $response, $dbForInternal, $dbForExternal,$database, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal*/
/** @var Utopia\Database\Database $dbForExternal*/
/** @var Appwrite\Event\Event $database */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
// Ensure attribute default is within range
$min = (is_null($min)) ? PHP_FLOAT_MIN : \floatval($min);
$min = (is_null($min)) ? -PHP_FLOAT_MAX : \floatval($min);
$max = (is_null($max)) ? PHP_FLOAT_MAX : \floatval($max);
if ($min > $max) {
@ -1018,7 +1078,7 @@ App::post('/v1/database/collections/:collectionId/attributes/float')
'min' => $min,
'max' => $max,
],
]), $response, $dbForInternal, $database, $audits, $usage);
]), $response, $dbForInternal, $dbForExternal, $database, $audits, $usage);
$formatOptions = $attribute->getAttribute('formatOptions', []);
@ -1049,10 +1109,11 @@ App::post('/v1/database/collections/:collectionId/attributes/boolean')
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForInternal')
->inject('dbForExternal')
->inject('database')
->inject('audits')
->inject('usage')
->action(function ($collectionId, $attributeId, $required, $default, $array, $response, $dbForInternal, $database, $audits, $usage) {
->action(function ($collectionId, $attributeId, $required, $default, $array, $response, $dbForInternal, $dbForExternal, $database, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal*/
/** @var Appwrite\Event\Event $database */
@ -1066,7 +1127,7 @@ App::post('/v1/database/collections/:collectionId/attributes/boolean')
'required' => $required,
'default' => $default,
'array' => $array,
]), $response, $dbForInternal, $database, $audits, $usage);
]), $response, $dbForInternal, $dbForExternal, $database, $audits, $usage);
$response->dynamic($attribute, Response::MODEL_ATTRIBUTE_BOOLEAN);
});
@ -1184,13 +1245,15 @@ App::delete('/v1/database/collections/:collectionId/attributes/:attributeId')
->param('attributeId', '', new Key(), 'Attribute ID.')
->inject('response')
->inject('dbForInternal')
->inject('dbForExternal')
->inject('database')
->inject('events')
->inject('audits')
->inject('usage')
->action(function ($collectionId, $attributeId, $response, $dbForInternal, $database, $events, $audits, $usage) {
->action(function ($collectionId, $attributeId, $response, $dbForInternal, $dbForExternal, $database, $events, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Utopia\Database\Database $dbForExternal */
/** @var Appwrite\Event\Event $database */
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Event\Event $audits */
@ -1214,6 +1277,7 @@ App::delete('/v1/database/collections/:collectionId/attributes/:attributeId')
}
$dbForInternal->deleteCachedDocument('collections', $collectionId);
$dbForExternal->deleteCachedCollection($collection->getId());
$database
->setParam('type', DATABASE_TYPE_DELETE_ATTRIBUTE)
@ -1654,11 +1718,11 @@ App::get('/v1/database/collections/:collectionId/documents')
return Query::parse($query);
}, $queries);
// TODO@kodumbeats use strict query validation
$validator = new QueriesValidator(new QueryValidator($collection->getAttribute('attributes', [])), $collection->getAttribute('indexes', []), false);
if (!$validator->isValid($queries)) {
throw new Exception($validator->getDescription(), 400);
if (!empty($queries)) {
$validator = new QueriesValidator(new QueryValidator($collection->getAttribute('attributes', [])), $collection->getAttribute('indexes', []), true);
if (!$validator->isValid($queries)) {
throw new Exception($validator->getDescription(), 400);
}
}
$cursorDocument = null;
@ -1747,6 +1811,113 @@ App::get('/v1/database/collections/:collectionId/documents/:documentId')
$response->dynamic($document, Response::MODEL_DOCUMENT);
});
App::get('/v1/database/collections/:collectionId/documents/:documentId/logs')
->desc('List Document Logs')
->groups(['api', 'database'])
->label('scope', 'documents.read')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'database')
->label('sdk.method', 'listDocumentLogs')
->label('sdk.description', '/docs/references/database/get-document-logs.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_LOG_LIST)
->param('collectionId', '', new UID(), 'Collection unique ID.')
->param('documentId', null, new UID(), 'Document unique ID.')
->param('limit', 25, new Range(0, 100), 'Maximum number of logs to return in response. Use this value to manage pagination. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, 900000000), 'Offset value. The default value is 0. Use this param to manage pagination.', true)
->inject('response')
->inject('dbForInternal')
->inject('dbForExternal')
->inject('locale')
->inject('geodb')
->action(function ($collectionId, $documentId, $limit, $offset, $response, $dbForInternal, $dbForExternal, $locale, $geodb) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Utopia\Database\Database $dbForExternal */
/** @var Utopia\Locale\Locale $locale */
/** @var MaxMind\Db\Reader $geodb */
$collection = $dbForInternal->getDocument('collections', $collectionId);
if ($collection->isEmpty()) {
throw new Exception('Collection not found', 404);
}
$document = $dbForExternal->getDocument($collectionId, $documentId);
if ($document->isEmpty()) {
throw new Exception('No document found', 404);
}
$audit = new Audit($dbForInternal);
$resource = 'document/'.$document->getId();
$logs = $audit->getLogsByResource($resource, $limit, $offset);
$output = [];
foreach ($logs as $i => &$log) {
$log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN';
$dd = new DeviceDetector($log['userAgent']);
$dd->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
$dd->parse();
$os = $dd->getOs();
$osCode = (isset($os['short_name'])) ? $os['short_name'] : '';
$osName = (isset($os['name'])) ? $os['name'] : '';
$osVersion = (isset($os['version'])) ? $os['version'] : '';
$client = $dd->getClient();
$clientType = (isset($client['type'])) ? $client['type'] : '';
$clientCode = (isset($client['short_name'])) ? $client['short_name'] : '';
$clientName = (isset($client['name'])) ? $client['name'] : '';
$clientVersion = (isset($client['version'])) ? $client['version'] : '';
$clientEngine = (isset($client['engine'])) ? $client['engine'] : '';
$clientEngineVersion = (isset($client['engine_version'])) ? $client['engine_version'] : '';
$output[$i] = new Document([
'event' => $log['event'],
'userId' => $log['userId'],
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
'ip' => $log['ip'],
'time' => $log['time'],
'osCode' => $osCode,
'osName' => $osName,
'osVersion' => $osVersion,
'clientType' => $clientType,
'clientCode' => $clientCode,
'clientName' => $clientName,
'clientVersion' => $clientVersion,
'clientEngine' => $clientEngine,
'clientEngineVersion' => $clientEngineVersion,
'deviceName' => $dd->getDeviceName(),
'deviceBrand' => $dd->getBrandName(),
'deviceModel' => $dd->getModel(),
]);
$record = $geodb->get($log['ip']);
if ($record) {
$output[$i]['countryCode'] = $locale->getText('countries.'.strtolower($record['country']['iso_code']), false) ? \strtolower($record['country']['iso_code']) : '--';
$output[$i]['countryName'] = $locale->getText('countries.'.strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown'));
} else {
$output[$i]['countryCode'] = '--';
$output[$i]['countryName'] = $locale->getText('locale.country.unknown');
}
}
$response->dynamic(new Document([
'sum' => $audit->countLogsByResource($resource),
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
App::patch('/v1/database/collections/:collectionId/documents/:documentId')
->desc('Update Document')
->groups(['api', 'database'])
@ -1901,6 +2072,7 @@ App::delete('/v1/database/collections/:collectionId/documents/:documentId')
}
$dbForExternal->deleteDocument($collectionId, $documentId);
$dbForExternal->deleteCachedDocument($collectionId, $documentId);
$usage
->setParam('database.documents.delete', 1)

View file

@ -176,7 +176,7 @@ App::get('/v1/functions/:functionId/usage')
$usage = [];
if(App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') {
$period = [
$periods = [
'24h' => [
'period' => '30m',
'limit' => 48,
@ -203,12 +203,15 @@ App::get('/v1/functions/:functionId/usage')
$stats = [];
Authorization::skip(function() use ($dbForInternal, $period, $range, $metrics, &$stats) {
Authorization::skip(function() use ($dbForInternal, $periods, $range, $metrics, &$stats) {
foreach ($metrics as $metric) {
$limit = $periods[$range]['limit'];
$period = $periods[$range]['period'];
$requestDocs = $dbForInternal->find('stats', [
new Query('period', Query::TYPE_EQUAL, [$period[$range]['period']]),
new Query('period', Query::TYPE_EQUAL, [$period]),
new Query('metric', Query::TYPE_EQUAL, [$metric]),
], $period[$range]['limit'], 0, ['time'], [Database::ORDER_DESC]);
], $limit, 0, ['time'], [Database::ORDER_DESC]);
$stats[$metric] = [];
foreach ($requestDocs as $requestDoc) {
@ -217,15 +220,30 @@ App::get('/v1/functions/:functionId/usage')
'date' => $requestDoc->getAttribute('time'),
];
}
// backfill metrics with empty values for graphs
$backfill = $limit - \count($requestDocs);
while ($backfill > 0) {
$last = $limit - $backfill - 1; // array index of last added metric
$diff = match($period) { // convert period to seconds for unix timestamp math
'30m' => 1800,
'1d' => 86400,
};
$stats[$metric][] = [
'value' => 0,
'date' => ($stats[$metric][$last]['date'] ?? \time()) - $diff, // time of last metric minus period
];
$backfill--;
}
$stats[$metric] = array_reverse($stats[$metric]);
}
});
$usage = new Document([
'range' => $range,
'functions.executions' => $stats["functions.$functionId.executions"],
'functions.failures' => $stats["functions.$functionId.failures"],
'functions.compute' => $stats["functions.$functionId.compute"]
'functionsExecutions' => $stats["functions.$functionId.executions"],
'functionsFailures' => $stats["functions.$functionId.failures"],
'functionsCompute' => $stats["functions.$functionId.compute"]
]);
}

View file

@ -255,7 +255,7 @@ App::get('/v1/projects/:projectId/usage')
$usage = [];
if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') {
$period = [
$periods = [
'24h' => [
'period' => '30m',
'limit' => 48,
@ -288,13 +288,16 @@ App::get('/v1/projects/:projectId/usage')
$stats = [];
Authorization::skip(function() use ($dbForInternal, $period, $range, $metrics, &$stats) {
Authorization::skip(function() use ($dbForInternal, $periods, $range, $metrics, &$stats) {
foreach ($metrics as $metric) {
$limit = $periods[$range]['limit'];
$period = $periods[$range]['period'];
$requestDocs = $dbForInternal->find('stats', [
new Query('period', Query::TYPE_EQUAL, [$period[$range]['period']]),
new Query('period', Query::TYPE_EQUAL, [$period]),
new Query('metric', Query::TYPE_EQUAL, [$metric]),
], $period[$range]['limit'], 0, ['time'], [Database::ORDER_DESC]);
], $limit, 0, ['time'], [Database::ORDER_DESC]);
$stats[$metric] = [];
foreach ($requestDocs as $requestDoc) {
$stats[$metric][] = [
@ -302,8 +305,23 @@ App::get('/v1/projects/:projectId/usage')
'date' => $requestDoc->getAttribute('time'),
];
}
// backfill metrics with empty values for graphs
$backfill = $limit - \count($requestDocs);
while ($backfill > 0) {
$last = $limit - $backfill - 1; // array index of last added metric
$diff = match($period) { // convert period to seconds for unix timestamp math
'30m' => 1800,
'1d' => 86400,
};
$stats[$metric][] = [
'value' => 0,
'date' => ($stats[$metric][$last]['date'] ?? \time()) - $diff, // time of last metric minus period
];
$backfill--;
}
$stats[$metric] = array_reverse($stats[$metric]);
}
}
});
$usage = new Document([

View file

@ -1461,7 +1461,7 @@ App::get('/v1/storage/usage')
$backfill--;
}
$stats[$metric] = array_reverse($stats[$metric]);
}
}
});
$usage = new Document([
@ -1572,7 +1572,7 @@ App::get('/v1/storage/:bucketId/usage')
$backfill--;
}
$stats[$metric] = array_reverse($stats[$metric]);
}
}
});
$usage = new Document([

View file

@ -259,12 +259,14 @@ App::get('/v1/users/:userId/logs')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_LOG_LIST)
->param('userId', '', new UID(), 'User unique ID.')
->param('limit', 25, new Range(0, 100), 'Maximum number of logs to return in response. Use this value to manage pagination. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, 900000000), 'Offset value. The default value is 0. Use this param to manage pagination.', true)
->inject('response')
->inject('dbForInternal')
->inject('locale')
->inject('geodb')
->inject('usage')
->action(function ($userId, $response, $dbForInternal, $locale, $geodb, $usage) {
->action(function ($userId, $limit, $offset, $response, $dbForInternal, $locale, $geodb, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Database $dbForInternal */
@ -279,8 +281,7 @@ App::get('/v1/users/:userId/logs')
}
$audit = new Audit($dbForInternal);
$logs = $audit->getLogsByUserAndEvents($user->getId(), [
$auditEvents = [
'account.create',
'account.delete',
'account.update.name',
@ -296,7 +297,9 @@ App::get('/v1/users/:userId/logs')
'teams.membership.create',
'teams.membership.update',
'teams.membership.delete',
]);
];
$logs = $audit->getLogsByUserAndEvents($user->getId(), $auditEvents, $limit, $offset);
$output = [];
@ -355,7 +358,11 @@ App::get('/v1/users/:userId/logs')
$usage
->setParam('users.read', 1)
;
$response->dynamic(new Document(['logs' => $output]), Response::MODEL_LOG_LIST);
$response->dynamic(new Document([
'sum' => $audit->countLogsByUserAndEvents($user->getId(), $auditEvents),
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
App::patch('/v1/users/:userId/status')
@ -780,7 +787,7 @@ App::get('/v1/users/usage')
$usage = [];
if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') {
$period = [
$periods = [
'24h' => [
'period' => '30m',
'limit' => 48,
@ -812,12 +819,15 @@ App::get('/v1/users/usage')
$stats = [];
Authorization::skip(function() use ($dbForInternal, $period, $range, $metrics, &$stats) {
Authorization::skip(function() use ($dbForInternal, $periods, $range, $metrics, &$stats) {
foreach ($metrics as $metric) {
$limit = $periods[$range]['limit'];
$period = $periods[$range]['period'];
$requestDocs = $dbForInternal->find('stats', [
new Query('period', Query::TYPE_EQUAL, [$period[$range]['period']]),
new Query('period', Query::TYPE_EQUAL, [$period]),
new Query('metric', Query::TYPE_EQUAL, [$metric]),
], $period[$range]['limit'], 0, ['time'], [Database::ORDER_DESC]);
], $limit, 0, ['time'], [Database::ORDER_DESC]);
$stats[$metric] = [];
foreach ($requestDocs as $requestDoc) {
@ -826,20 +836,36 @@ App::get('/v1/users/usage')
'date' => $requestDoc->getAttribute('time'),
];
}
// backfill metrics with empty values for graphs
$backfill = $limit - \count($requestDocs);
while ($backfill > 0) {
$last = $limit - $backfill - 1; // array index of last added metric
$diff = match($period) { // convert period to seconds for unix timestamp math
'30m' => 1800,
'1d' => 86400,
};
$stats[$metric][] = [
'value' => 0,
'date' => ($stats[$metric][$last]['date'] ?? \time()) - $diff, // time of last metric minus period
];
$backfill--;
}
$stats[$metric] = array_reverse($stats[$metric]);
}
});
$usage = new Document([
'range' => $range,
'users.count' => $stats["users.count"],
'users.create' => $stats["users.create"],
'users.read' => $stats["users.read"],
'users.update' => $stats["users.update"],
'users.delete' => $stats["users.delete"],
'sessions.create' => $stats["users.sessions.create"],
'sessions.provider.create' => $stats["users.sessions.$provider.create"],
'sessions.delete' => $stats["users.sessions.delete"]
'usersCount' => $stats["users.count"],
'usersCreate' => $stats["users.create"],
'usersRead' => $stats["users.read"],
'usersUpdate' => $stats["users.update"],
'usersDelete' => $stats["users.delete"],
'sessionsCreate' => $stats["users.sessions.create"],
'sessionsProviderCreate' => $stats["users.sessions.$provider.create"],
'sessionsDelete' => $stats["users.sessions.delete"]
]);
}

View file

@ -218,6 +218,10 @@ App::get('/console/database/collection')
$logs
->setParam('interval', App::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 0))
->setParam('method', 'database.listCollectionLogs')
->setParam('params', [
'collection-id' => '{{router.params.id}}',
])
;
$page = new View(__DIR__.'/../../views/console/database/collection.phtml');
@ -245,14 +249,45 @@ App::get('/console/database/document')
->action(function ($collection, $layout) {
/** @var Utopia\View $layout */
$logs = new View(__DIR__.'/../../views/console/comps/logs.phtml');
$logs
->setParam('interval', App::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 0))
->setParam('method', 'database.listDocumentLogs')
->setParam('params', [
'collection-id' => '{{router.params.collection}}',
'document-id' => '{{router.params.id}}',
])
;
$page = new View(__DIR__.'/../../views/console/database/document.phtml');
$searchFiles = new View(__DIR__.'/../../views/console/database/search/files.phtml');
$searchDocuments = new View(__DIR__.'/../../views/console/database/search/documents.phtml');
$page
->setParam('new', false)
->setParam('collection', $collection)
->setParam('searchFiles', $searchFiles)
->setParam('searchDocuments', $searchDocuments)
->setParam('logs', $logs)
;
$layout
->setParam('title', APP_NAME.' - Database Document')
->setParam('body', $page);
});
App::get('/console/database/document/new')
->groups(['web', 'console'])
->label('permission', 'public')
->label('scope', 'console')
->param('collection', '', new UID(), 'Collection unique ID.')
->inject('layout')
->action(function ($collection, $layout) {
/** @var Utopia\View $layout */
$page = new View(__DIR__.'/../../views/console/database/document.phtml');
$page
->setParam('new', true)
->setParam('collection', $collection)
->setParam('logs', new View())
;
$layout

View file

@ -280,17 +280,19 @@ $cli
$max = 10;
$sleep = 1;
$database = $client->selectDB('telegraf');
do { // check if telegraf database is ready
$attempts++;
if(!in_array('telegraf', $client->listDatabases())) {
try {
$attempts++;
$database = $client->selectDB('telegraf');
if(in_array('telegraf', $client->listDatabases())) {
break; // leave the do-while if successful
}
} catch (\Throwable $th) {
Console::warning("InfluxDB not ready. Retrying connection ({$attempts})...");
if($attempts >= $max) {
if ($attempts >= $max) {
throw new \Exception('InfluxDB database not ready yet');
}
sleep($sleep);
} else {
break; // leave the do-while if successful
}
} while ($attempts < $max);
@ -309,17 +311,20 @@ $cli
$filters = $options['filters'] ?? []; // Some metrics might have additional filters, like function's status
if (!empty($filters)) {
$filters = ' AND ' . implode(' AND ', array_map(function ($filter, $value) {
return '"' . $filter . '"=\'' . $value . '\'';
return "\"{$filter}\"='{$value}'";
}, array_keys($filters), array_values($filters)));
} else {
$filters = '';
}
$result = $database->query('SELECT sum(value) AS "value" FROM "' . $table . '" WHERE time > \'' . $start . '\' AND time < \'' . $end . '\' AND "metric_type"=\'counter\'' . (empty($filters) ? '' : $filters) . ' GROUP BY time(' . $period['key'] . '), "projectId"' . $groupBy . ' FILL(null)');
$query = "SELECT sum(value) AS \"value\" FROM \"{$table}\" WHERE \"time\" > '{$start}' AND \"time\" < '{$end}' AND \"metric_type\"='counter' {$filters} GROUP BY time({$period['key']}), \"projectId\" {$groupBy} FILL(null)";
$result = $database->query($query);
$points = $result->getPoints();
foreach ($points as $point) {
$projectId = $point['projectId'];
if (!empty($projectId) && $projectId != 'console') {
if (!empty($projectId) && $projectId !== 'console') {
$dbForProject->setNamespace('project_' . $projectId . '_internal');
$metricUpdated = $metric;
@ -347,8 +352,11 @@ $cli
'type' => 0,
]));
} else {
$dbForProject->updateDocument('stats', $document->getId(),
$document->setAttribute('value', $value));
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $value)
);
}
$latestTime[$metric][$period['key']] = $time;
} catch (\Exception $e) { // if projects are deleted this might fail
@ -364,7 +372,7 @@ $cli
* Aggregate MariaDB every 15 minutes
* Some of the queries here might contain full-table scans.
*/
if ($iterations % 30 == 0) { // Every 15 minutes aggregate number of objects in database
if ($iterations % 30 === 0) { // Every 15 minutes aggregate number of objects in database
$latestProject = null;
@ -376,7 +384,7 @@ $cli
do { // list projects
try {
$attempts++;
$projects = $dbForConsole->find('projects', [], 100, cursor:$latestProject);
$projects = $dbForConsole->find('projects', [], 100, cursor: $latestProject);
break; // leave the do-while if successful
} catch (\Exception $e) {
Console::warning("Console DB not ready yet. Retrying ({$attempts})...");
@ -413,10 +421,13 @@ $cli
'type' => 1,
]));
} else {
$dbForProject->updateDocument('stats', $document->getId(),
$document->setAttribute('value', $storageTotal));
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $storageTotal)
);
}
$time = (int) (floor(time() / 86400) * 86400); // Time rounded to nearest day
$id = \md5($time . '_1d_storage.total'); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
@ -430,8 +441,11 @@ $cli
'type' => 1,
]));
} else {
$dbForProject->updateDocument('stats', $document->getId(),
$document->setAttribute('value', $storageTotal));
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $storageTotal)
);
}
$collections = [
@ -483,8 +497,11 @@ $cli
'type' => 1,
]));
} else {
$dbForProject->updateDocument('stats', $document->getId(),
$document->setAttribute('value', $count));
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count)
);
}
$time = (int) (floor(time() / 86400) * 86400); // Time rounded to nearest day
@ -500,8 +517,11 @@ $cli
'type' => 1,
]));
} else {
$dbForProject->updateDocument('stats', $document->getId(),
$document->setAttribute('value', $count));
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count)
);
}
$subCollections = $options['subCollections'] ?? [];
@ -516,7 +536,7 @@ $cli
do { // Loop over all the parent collection document for each sub collection
$dbForProject->setNamespace("project_{$projectId}_{$options['namespace']}");
$parents = $dbForProject->find($collection, [], 100, cursor:$latestParent); // Get all the parents for the sub collections for example for documents, this will get all the collections
$parents = $dbForProject->find($collection, [], 100, cursor: $latestParent); // Get all the parents for the sub collections for example for documents, this will get all the collections
if (empty($parents)) {
continue;
@ -547,8 +567,11 @@ $cli
'type' => 1,
]));
} else {
$dbForProject->updateDocument('stats', $document->getId(),
$document->setAttribute('value', $count));
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count)
);
}
$time = (int) (floor(time() / 86400) * 86400); // Time rounded to nearest day
@ -564,8 +587,11 @@ $cli
'type' => 1,
]));
} else {
$dbForProject->updateDocument('stats', $document->getId(),
$document->setAttribute('value', $count));
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count)
);
}
// check if sum calculation is required
@ -641,8 +667,11 @@ $cli
'type' => 1,
]));
} else {
$dbForProject->updateDocument('stats', $document->getId(),
$document->setAttribute('value', $count));
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count)
);
}
$time = (int) (floor(time() / 86400) * 86400); // Time rounded to nearest day
@ -658,8 +687,11 @@ $cli
'type' => 1,
]));
} else {
$dbForProject->updateDocument('stats', $document->getId(),
$document->setAttribute('value', $count));
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count)
);
}
}

View file

@ -301,7 +301,7 @@
data-name="securityLogs"
data-event="load">
<div class="box">
<div class="box margin-bottom">
<table class="vertical small">
<thead>
<tr>
@ -330,6 +330,33 @@
</tbody>
</table>
</div>
<div class="pull-end text-align-center paging">
<form
data-service="account.getLogs"
data-event="submit"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-offset="{{router.params.offset}}"
data-scope="console"
data-name="securityLogs"
data-success="state"
data-success-param-state-keys="offset">
<button name="offset" data-paging-back data-offset="{{router.params.offset}}" data-sum="{{securityLogs.sum}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
</form>
<span data-ls-bind="{{router.params.offset|pageCurrent}} / {{securityLogs.sum|pageTotal}}"></span>
<form
data-service="account.getLogs"
data-event="submit"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-offset="{{router.params.offset}}"
data-scope="console"
data-name="securityLogs"
data-success="state"
data-success-param-state-keys="offset">
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-sum="{{securityLogs.sum}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>
</div>
</li>
</ul>

View file

@ -211,8 +211,9 @@
required
maxlength="36"
class=""
pattern="^[a-zA-Z0-9][a-zA-Z0-9_-]{1,36}$"
name="projectId" />
<label>Name</label>
<input type="text" class="full-width margin-bottom-xl" name="name" required autocomplete="off" maxlength="128" />

View file

@ -1,78 +1,96 @@
<?php
$interval = floor((int)$this->getParam('interval', 0) / 86400);
$method = $this->getParam('method', '');
$params = $this->getParam('params', []);
?>
<div
data-service="database.listCollectionLogs"
data-param-collection-id="{{router.params.id}}"
data-service="<?php echo $method; ?>"
<?php foreach($params as $key => $value): ?>
data-param-<?php echo $key; ?>="<?php echo $value; ?>"
<?php endforeach; ?>
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-scope="sdk"
data-event="load"
data-name="project-collection-logs">
<div class="box margin-bottom">
<div data-ls-if="0 == {{project-collection-logs.logs.length}}">
data-name="logs">
<div data-ls-if="0 == {{logs.logs.length}}">
<div class="box margin-bottom">
<h3 class="margin-bottom-small text-bold">No Logs Found</h3>
<p class="margin-bottom-no">Logs are retained for <?php echo $this->escape($interval); ?> days.</p>
</div>
<table class="vertical small" data-ls-if="0 != {{project-collection-logs.logs.length}}">
<thead>
<tr>
<th width="120">Date</th>
<th width="180">Initiator</th>
<th>Event</th>
<th width="110">Location</th>
<th width="90">IP</th>
</tr>
</thead>
<tbody data-ls-loop="project-collection-logs.logs" data-ls-as="log" class="text-size-small">
<tr>
<td data-title="Date: "><span class="text-fade" data-ls-bind="{{log.time|dateTime}}"></span></td>
<td data-title="Initiator: ">
<span data-ls-if="{{log.userName|escape}} !== '' && {{log.mode}} === ''"><i class="icon-user"></i>&nbsp; <a data-ls-attrs="href=/console/users/user?id={{log.userId}}&project={{router.params.project}}" data-ls-bind="{{log.userName}}"></a></span>
<span data-ls-if="{{log.userName|escape}} === '' && {{log.userEmail}} !== '' && {{log.mode}} !== 'key'"><i class="icon-user"></i>&nbsp; Unknown</span>
<span data-ls-if="{{log.userName|escape}} === '' && {{log.userEmail}} === '' && {{log.mode}} !== 'key'"><i class="icon-user"></i>&nbsp; Anonymous User</span>
<span data-ls-if="{{log.mode}} === 'admin'">
<img src="" data-ls-attrs="src={{log.userName|avatar}}" data-size="45" alt="User Avatar" class="avatar xxs inline margin-end-small" loading="lazy" width="30" height="30" /> <span data-ls-bind="{{log.userName}}"></span> <span class="text-fade text-size-xs">(Admin)</span>
</span>
<span data-ls-if="{{log.mode}} === 'key'"> <i class="icon-key"></i>&nbsp; API Key</span>
</td>
<td data-title="Event: "><span data-ls-bind="{{log.event}}"></span></td>
<td data-title="Location: ">
<img onerror="this.onerror=null;this.className='avatar xxs hide'" data-ls-attrs="src={{env.API}}/avatars/flags/{{log.countryCode}}?width=80&height=80&project={{env.PROJECT}}" class="avatar xxs inline margin-end-small" />
<span data-ls-bind="{{log.countryName}}"></span>
</td>
<td data-title="IP: "><span data-ls-bind="{{log.ip}}"></span></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="pull-end text-align-center paging">
<form
data-service="database.listCollectionLogs"
data-event="submit"
data-param-collection-id="{{router.params.id}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="sdk"
data-name="project-collection-logs"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-back data-offset="{{router.params.offset}}" data-sum="{{project-collection-logs.sum}}" class="margin-end-small round small" aria-label="Back"><i class="icon-left-open"></i></button>
</form>
<div data-ls-if="0 != {{logs.sum}}">
<div class="margin-bottom-small margin-top-negative text-align-end text-size-small text-fade">Showing logs from the last <?php echo $this->escape($interval); ?> days</div>
<form
data-service="database.listCollectionLogs"
data-event="submit"
data-param-collection-id="{{router.params.id}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="sdk"
data-name="project-collection-logs"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-sum="{{project-collection-logs.sum}}" class="margin-start-small round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>
<div class="box margin-bottom">
<table class="vertical small">
<thead>
<tr>
<th width="120">Date</th>
<th width="180">By</th>
<th>Event</th>
<th width="110">Location</th>
<th width="90">IP</th>
</tr>
</thead>
<tbody data-ls-loop="logs.logs" data-ls-as="log" class="text-size-small">
<tr>
<td data-title="Date: "><span class="text-fade" data-ls-bind="{{log.time|dateTime}}"></span></td>
<td data-title="By: ">
<span data-ls-if="{{log.userName|escape}} !== '' && {{log.mode}} === ''"><i class="icon-user"></i>&nbsp; <a data-ls-attrs="href=/console/users/user?id={{log.userId}}&project={{router.params.project}}" data-ls-bind="{{log.userName}}"></a></span>
<span data-ls-if="{{log.userName|escape}} === '' && {{log.userEmail}} !== '' && {{log.mode}} !== 'key'"><i class="icon-user"></i>&nbsp; Unknown</span>
<span data-ls-if="{{log.userName|escape}} === '' && {{log.userEmail}} === '' && {{log.mode}} !== 'key'"><i class="icon-user"></i>&nbsp; Anonymous User</span>
<span data-ls-if="{{log.mode}} === 'admin'">
<img src="" data-ls-attrs="src={{log.userName|avatar}}" data-size="45" alt="User Avatar" class="avatar xxs inline margin-end-small" loading="lazy" width="30" height="30" /> <span data-ls-bind="{{log.userName}}"></span> <span class="text-fade text-size-xs">(Admin)</span>
</span>
<span data-ls-if="{{log.mode}} === 'key'"> <i class="icon-key"></i>&nbsp; API Key</span>
</td>
<td data-title="Event: "><span data-ls-bind="{{log.event}}"></span></td>
<td data-title="Location: ">
<img onerror="this.onerror=null;this.className='avatar xxs hide'" data-ls-attrs="src={{env.API}}/avatars/flags/{{log.countryCode}}?width=80&height=80&project={{env.PROJECT}}" class="avatar xxs inline margin-end-small" />
<span data-ls-bind="{{log.countryName}}"></span>
</td>
<td data-title="IP: "><span data-ls-bind="{{log.ip}}"></span></td>
</tr>
</tbody>
</table>
</div>
<div class="pull-end text-align-center paging">
<form
data-service="<?php echo $method; ?>"
<?php foreach($params as $key => $value): ?>
data-param-<?php echo $key; ?>="<?php echo $value; ?>"
<?php endforeach; ?>
data-event="submit"
data-param-collection-id="{{router.params.id}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="sdk"
data-name="logs"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-back data-offset="{{router.params.offset}}" data-sum="{{logs.sum}}" class="margin-end-small round small" aria-label="Back"><i class="icon-left-open"></i></button>
</form>
<span data-ls-bind="{{router.params.offset|pageCurrent}} / {{logs.sum|pageTotal}}"></span>
<form
data-service="<?php echo $method; ?>"
<?php foreach($params as $key => $value): ?>
data-param-<?php echo $key; ?>="<?php echo $value; ?>"
<?php endforeach; ?>
data-event="submit"
data-param-collection-id="{{router.params.id}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="sdk"
data-name="logs"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-sum="{{logs.sum}}" class="margin-start-small round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>
</div>
</div>

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,16 @@
<?php
$new = $this->getParam('new', false);
$logs = $this->getParam('logs', null);
?>
<div
data-service="database.getCollection"
data-param-collection-id="{{router.params.collection}}"
data-scope="sdk"
data-event="load,database.updateDocument"
data-name="project-collection">
<div
data-service="database.getDocument"
data-param-collection-id="{{router.params.collection}}"
@ -27,7 +33,7 @@
<div data-ui-modal class="modal width-large box close" data-button-hide="on" data-open-event="open-json">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h2>JSON View</h2>
<div class="margin-bottom">
@ -40,9 +46,9 @@
<div class="zone xl margin-bottom-no">
<ul class="phases clear" data-ui-phases data-selected="{{router.params.tab}}">
<li data-state="/console/database/document?id={{router.params.id}}&collection={{router.params.collection}}&project={{router.params.project}}">
<h2>Overview</h2>
<h2 class="margin-bottom">Overview</h2>
<div class="row responsive margin-top-negative">
<div class="row responsive">
<div class="col span-8 margin-bottom">
<form
data-analytics
@ -54,28 +60,74 @@
data-name="project-document"
data-scope="sdk"
data-event="submit"
data-success="alert,trigger"
data-success-param-alert-text="Updated document successfully"
data-success="trigger,redirect"
data-success-param-trigger-events="database.updateDocument"
data-success-param-redirect-url="/console/database/document?id={{serviceData.$id}}&collection={{project-collection.$id}}&project={{router.params.project}}"
data-failure="alert"
data-failure-param-alert-text="Failed to update document"
data-failure-param-alert-classname="error">
<input type="hidden" name="collectionId" data-ls-bind="{{project-collection.$id}}" />
<input type="hidden" name="documentId" data-ls-bind="{{project-document.$id}}" />
<label>&nbsp;</label>
<input type="hidden" name="collectionId" data-ls-bind="{{project-collection.$id}}" />
<?php if(!$new): ?><input type="hidden" name="documentId" data-ls-bind="{{project-document.$id}}" /><?php endif; ?>
<div class="box">
<fieldset name="data" data-cast-to="object">
<?php if($new): ?>
<label for="documentId">Document ID</label>
<input
type="hidden"
data-custom-id
data-id-type="auto"
data-validator="database.getDocument"
required
maxlength="36"
name="documentId"
id="documentId"
maxlength="36"
pattern="^[a-zA-Z0-9][a-zA-Z0-9_-]{1,36}$" />
<?php endif; ?>
<fieldset name="data" data-cast-to="object">
<ul data-ls-loop="project-collection.attributes" data-ls-as="attribute">
<li>
<li data-ls-if="{{attribute.status}} === 'available'">
<label>
<div data-ls-bind="{{attribute.key}}" class="margin-bottom-tiny"></div>
<div data-ls-if="{{attribute.required}}" class="text-size-xs text-danger text-fade">required</div>
<div data-ls-if="!{{attribute.required}}" class="text-size-xs text-fade">optional</div>
</label>
<textarea data-ls-attrs="name={{attribute.key}}" data-ls-bind="{{project-document|documentAttribute}}"></textarea>
<div data-ls-if="!{{attribute.array}}">
<div data-ls-if="{{attribute.format}}" data-ls-template="template-{{attribute.format}}" data-type="script"></div>
<div data-ls-if="!{{attribute.format}}" data-ls-template="template-{{attribute.type}}" data-type="script"></div>
</div>
<div data-ls-if="{{attribute.array}}">
<input type="hidden" data-forms-required="{{attribute.required}}" data-ls-attrs="name={{attribute.key}}" data-cast-to="array-empty">
<div data-ls-loop="project-document.{{attribute.key}}" data-ls-as="item">
<div class="row responsive thin margin-bottom-tiny" data-forms-remove>
<div class="col span-11 margin-bottom-small">
<div data-ls-if="{{attribute.format}}" data-ls-template="template-{{attribute.format}}-array" data-type="script"></div>
<div data-ls-if="!{{attribute.format}}" data-ls-template="template-{{attribute.type}}-array" data-type="script"></div>
</div>
<div class="col span-1 margin-bottom-small">
<button type="button" data-remove class="dark danger small round pull-end" style="margin-top: 10px;"><i class="icon-cancel"></i></button>
</div>
</div>
</div>
<div data-ls-attrs="id=attribute-{{attribute.key}}"></div>
<div class="margin-bottom">
<div data-forms-clone data-label="Add Attribute" data-target="attribute-{{attribute.key}}" data-first="0">
<div class="row responsive thin margin-bottom-tiny" data-forms-remove>
<div class="col span-11 margin-bottom-small">
<div data-ls-if="{{attribute.format}}" data-ls-template="template-{{attribute.format}}-array" data-type="script"></div>
<div data-ls-if="!{{attribute.format}}" data-ls-template="template-{{attribute.type}}-array" data-type="script"></div>
</div>
<div class="col span-1 margin-bottom-small">
<button type="button" data-remove class="dark danger small round pull-end" style="margin-top: 10px;"><i class="icon-cancel"></i></button>
</div>
</div>
</div>
</div>
</div>
</li>
</ul>
</fieldset>
@ -145,10 +197,82 @@
</div>
</div>
</li>
<!-- <li data-ls-if="{{project-document.$id}}" data-state="/console/database/document/activity?id={{router.params.id}}&collection={{router.params.collection}}&project={{router.params.project}}">
<h2>Activity</h2>
</li> -->
<?php if(!$new): ?>
<li data-state="/console/database/document/activity?id={{router.params.id}}&collection={{router.params.collection}}&project={{router.params.project}}">
<h2>Activity <span class="badge" data-ls-bind="{{logs.sum}}"></span></h2>
<?php echo $logs->render(); ?>
</li>
<?php endif; ?>
</ul>
</div>
</div>
</div>
</div>
<script type="text/html" id="template-string">
<textarea data-forms-text-resize data-forms-text-direction data-ls-attrs="placeholder={{attribute.default}},name={{attribute.key}}" data-forms-required="{{attribute.required}}" data-ls-bind="{{project-document|documentAttribute}}" data-cast-to="string"></textarea>
</script>
<script type="text/html" id="template-string-array">
<textarea data-forms-text-resize data-forms-text-direction data-ls-attrs="placeholder={{attribute.default}},name={{attribute.key}}" data-ls-bind="{{item}}" data-cast-to="string"></textarea>
</script>
<script type="text/html" id="template-integer">
<input type="number" step="1" data-ls-attrs="placeholder={{attribute.default}},name={{attribute.key}}" data-forms-required="{{attribute.required}}" data-ls-bind="{{project-document|documentAttribute}}" data-cast-to="integer" />
</script>
<script type="text/html" id="template-integer-array">
<input type="number" step="1" data-ls-attrs="placeholder={{attribute.default}},name={{attribute.key}}" data-ls-bind="{{item}}" data-cast-to="integer" />
</script>
<script type="text/html" id="template-double">
<input type="number" step="0.01" data-ls-attrs="placeholder={{attribute.default}},name={{attribute.key}}" data-forms-required="{{attribute.required}}" data-ls-bind="{{project-document|documentAttribute}}" data-cast-to="float" />
</script>
<script type="text/html" id="template-double-array">
<input type="number" step="0.01" data-ls-attrs="placeholder={{attribute.default}},name={{attribute.key}}" data-ls-bind="{{item}}" data-cast-to="float" />
</script>
<script type="text/html" id="template-boolean">
<input type="hidden" data-forms-switch data-ls-attrs="name={{attribute.key}}" data-ls-bind="{{project-document|documentAttribute}}" data-cast-to="boolean" class="margin-bottom-no" />
</script>
<script type="text/html" id="template-boolean-array">
<input type="hidden" data-forms-switch data-ls-attrs="name={{attribute.key}}" data-ls-bind="{{item}}" data-cast-to="boolean" class="margin-bottom-no" />
</script>
<script type="text/html" id="template-url">
<input type="url" data-ls-attrs="placeholder={{attribute.default}},name={{attribute.key}}" data-forms-required="{{attribute.required}}" data-ls-bind="{{project-document|documentAttribute}}" data-cast-to="string" />
</script>
<script type="text/html" id="template-url-array">
<input type="url" data-ls-attrs="placeholder={{attribute.default}},name={{attribute.key}}" data-ls-bind="{{item}}" data-cast-to="string" />
</script>
<script type="text/html" id="template-email">
<input type="email" data-ls-attrs="placeholder={{attribute.default}},name={{attribute.key}}" data-forms-required="{{attribute.required}}" data-ls-bind="{{project-document|documentAttribute}}" data-cast-to="string" />
</script>
<script type="text/html" id="template-email-array">
<input type="email" data-ls-attrs="placeholder={{attribute.default}},name={{attribute.key}}" data-ls-bind="{{item}}" data-cast-to="string" />
</script>
<script type="text/html" id="template-ip">
<input type="text" data-ls-attrs="placeholder={{attribute.default}},name={{attribute.key}}" data-forms-required="{{attribute.required}}" data-ls-bind="{{project-document|documentAttribute}}" data-cast-to="string" pattern="((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))" title="Enter a valid IPV4 or IPV6 address" />
</script>
<script type="text/html" id="template-ip-array">
<input type="text" data-ls-attrs="placeholder={{attribute.default}},name={{attribute.key}}" data-ls-bind="{{item}}" data-cast-to="string" pattern="((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))" title="Enter a valid IPV4 or IPV6 address" />
</script>
<script type="text/html" id="template-enum">
<select data-ls-attrs="name={{attribute.key}}" data-ls-loop="attribute.elements" data-ls-as="element" data-cast-to="string">
<option data-ls-attrs="value={{element}}" data-ls-bind="{{element}}" data-forms-selected="{{project-document|documentAttribute}}"></option>
</select>
</script>
<script type="text/html" id="template-enum-array">
<select data-ls-attrs="name={{attribute.key}}" data-ls-loop="attribute.elements" data-ls-as="element" data-cast-to="string">
<option data-ls-attrs="value={{element}}" data-ls-bind="{{element}}" data-forms-selected="{{item}}"></option>
</select>
</script>

View file

@ -102,6 +102,7 @@
data-validator="database.getCollection"
required
maxlength="36"
pattern="^[a-zA-Z0-9][a-zA-Z0-9_-]{1,36}$"
name="collectionId" />
<label for="collection-name">Name</label>
@ -118,9 +119,110 @@
</div>
</li>
<!-- <li data-state="/console/database/usage?project={{router.params.project}}">
<li data-state="/console/database/usage?project={{router.params.project}}">
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '90d'"
data-service="database.getUsage"
data-event="submit"
data-name="usage"
data-param-range="90d">
<button class="tick">90d</button>
</form>
<button class="tick pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} === '90d'" disabled>90d</button>
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '30d'"
data-service="database.getUsage"
data-event="submit"
data-name="usage">
<button class="tick">30d</button>
</form>
<button class="tick pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} === '30d'" disabled>30d</button>
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '24h'"
data-service="database.getUsage"
data-event="submit"
data-name="usage"
data-param-range="24h">
<button class="tick">24h</button>
</form>
<button class="tick pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} === '24h'" disabled>24h</button>
<h2>Usage</h2>
</li> -->
<div
data-service="database.getUsage"
data-event="load"
data-name="usage">
<h3 class="margin-bottom-tiny">Objects</h3>
<p class="text-fade">Count of collections and documents over time</p>
<div class="box margin-bottom-small">
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
<div class="chart margin-bottom-no">
<input
type="hidden"
data-ls-bind="{{usage}}"
data-forms-chart="Collections=collectionsCount,Documents=documentsCount"
data-show-y-axis="true"
data-height="140" />
</div>
</div>
</div>
<ul class="chart-notes margin-bottom-large">
<li>Total Collections <span data-ls-bind="({{usage.collectionsCount|statsGetLast|statsTotal}})"></span></li>
<li>Total Documents <span data-ls-bind="({{usage.documentsCount|statsGetLast|statsTotal}})"></span></li>
</ul>
<h3 class="margin-bottom-tiny">Collections</h3>
<p class="text-fade">Count of collections create, read, update and delete operations over time</p>
<div class="box margin-bottom-small">
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
<div class="chart margin-bottom-no">
<input
type="hidden"
data-ls-bind="{{usage}}"
data-forms-chart="Created=collectionsCreate,Read=collectionsRead,Updated=collectionsUpdate,Deleted=collectionsDelete"
data-show-y-axis="true"
data-colors="create,read,update,delete"
data-height="140" />
</div>
</div>
</div>
<ul class="chart-notes margin-bottom-large crud">
<li>Created</li>
<li>Read</li>
<li>Updated</li>
<li>Deleted</li>
</ul>
<h3 class="margin-bottom-tiny">Documents</h3>
<p class="text-fade">Count of documents create, read, update and delete operations over time</p>
<div class="box margin-bottom-small">
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
<div class="chart margin-bottom-no">
<input
type="hidden"
data-ls-bind="{{usage}}"
data-forms-chart="Created=documentsCreate,Read=documentsRead,Updated=documentsUpdate,Deleted=documentsDelete"
data-show-y-axis="true"
data-colors="create,read,update,delete"
data-height="140" />
</div>
</div>
</div>
<ul class="chart-notes margin-bottom-large crud">
<li>Created</li>
<li>Read</li>
<li>Updated</li>
<li>Deleted</li>
</ul>
</div>
</li>
</ul>
</div>
</div>

View file

@ -247,37 +247,37 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
<div class="box margin-bottom-small">
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
<div class="chart margin-bottom-no">
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="Executions=usage.executions.data" data-height="140" />
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="Executions=functionsExecutions" data-height="140" />
</div>
</div>
</div>
<ul class="chart-notes margin-bottom-large">
<li>Executions <span data-ls-bind="({{usage.executions.total}})"></span></li>
<li>Executions <span data-ls-bind="({{usage.functionsExecutions|statsGetLast|statsTotal}})"></span></li>
</ul>
<div class="box margin-bottom-small">
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
<div class="chart margin-bottom-no">
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="CPU Time (seconds)=usage.compute.data" data-colors="orange" data-height="140" />
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="CPU Time (seconds)=functionsCompute" data-colors="orange" data-height="140" />
</div>
</div>
</div>
<ul class="chart-notes margin-bottom-large">
<li class="orange">CPU Time <span data-ls-bind="({{usage.compute.total|seconds2hum}})"></span></li>
<li class="orange">CPU Time <span data-ls-bind="({{usage.functionsCompute|statsGetLast|seconds2hum}})"></span></li>
</ul>
<div class="box margin-bottom-small">
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
<div class="chart margin-bottom-no">
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="Failures=usage.failures.data" data-colors="red" data-height="140" />
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="Failures=functionsFailures" data-colors="red" data-height="140" />
</div>
</div>
</div>
<ul class="chart-notes margin-bottom-large">
<li class="red">Errors <span data-ls-bind="({{usage.failures.total}})"></span></li>
<li class="red">Errors <span data-ls-bind="({{usage.functionsFailures|statsGetLast|statsTotal}})"></span></li>
</ul>
</div>
</li>

View file

@ -113,6 +113,7 @@ $runtimes = $this->getParam('runtimes', []);
data-validator="functions.get"
required
maxlength="36"
pattern="^[a-zA-Z0-9][a-zA-Z0-9_-]{1,36}$"
name="functionId" />
<label for="name">Name</label>
@ -125,7 +126,7 @@ $runtimes = $this->getParam('runtimes', []);
<?php endforeach;?>
</select>
<input id="execute" name="execute" value="" hidden/>
<input id="execute" name="execute" value="" hidden />
<footer>
<button type="submit">Create</button> &nbsp; <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>

View file

@ -39,7 +39,7 @@ $home = $this->getParam('home', '');
<li class="margin-bottom">
<a data-ls-attrs="href=/console/home?project={{project.$id}}" class="box">
<div data-ls-bind="{{project.name}}" class="text-one-liner margin-bottom-tiny text-bold">&nbsp;</div>
<p data-ls-if="({{project.platforms.length}})" class="text-fade text-size-small" data-ls-bind="{{project.platforms.length}} apps"></p>
<p data-ls-if="(!{{project.platforms.length}})" class="text-fade text-size-small">&nbsp;</p>
@ -81,7 +81,7 @@ $home = $this->getParam('home', '');
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-sum="{{console-projects.sum}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>
<button data-ls-ui-trigger="create-project">Create Project</button>
</div>
@ -89,9 +89,9 @@ $home = $this->getParam('home', '');
<div class="col span-6 margin-bottom">
<div class="box line community">
<h3 class="margin-bottom-small text-size-normal">Join The Community</h3>
<p class="text-fade">Join Appwrite growing developers community channels.</p>
<a href="<?php echo APP_SOCIAL_TWITTER; ?>" target="_blank" rel="noopener" title="<?php echo APP_NAME;?> on Twitter"
data-analytics
data-analytics-event="click"
@ -122,7 +122,7 @@ $home = $this->getParam('home', '');
<div class="col span-6 margin-bottom">
<div class="box line">
<h3 class="margin-bottom-small text-size-normal">Read The Docs</h3>
<p class="text-fade">Take full advantage of Appwrite APIs and tools for your new project.</p>
<a data-ls-attrs="href={{env.HOME}}/docs" target="_blank" rel="noopener"><i class="icon-angle-circled-right margin-start-negative-tiny margin-end-tiny"></i> Full Documentation</a>

View file

@ -64,7 +64,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<!-- <div data-ls-if="0 !== {{console-domains|activeDomainsCount}}">
<label for="name">Custom API Endpoints</label>
<ul data-ls-loop="console-domains" data-ls-as="domain">
<li>
<div class="input-copy" data-ls-if="true === {{domain.verification}} && {{domain.certificateId}}">
@ -73,7 +73,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
</li>
</ul>
</div> -->
<button class="" type="submit">Update</button>
</form>
@ -110,14 +110,14 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<div data-ui-modal class="modal box close width-small height-small" data-button-text="Delete Project" data-button-class="danger reverse">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h3>Confirm Password</h3>
<hr />
<label>Password</label>
<input name="password" type="password" class="full-width" autocomplete="off" placeholder="" required>
<hr />
<button type="submit" class="margin-bottom-no danger fill">Delete Project</button>
@ -232,7 +232,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
</form>
<img src="<?php echo $this->escape($icon); ?>?buster=<?php echo APP_CACHE_BUSTER; ?>" alt="" class="pull-start provider margin-end" />
<span class="text-size-small text-bold"><?php echo $this->escape($name); ?></span>
<?php if($docs): ?>
@ -241,7 +241,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
</p>
<?php endif; ?>
</div>
<hr />
<div class="text-align-center text-size-small text-bold text-success" data-ls-if="!!({{console-project.serviceStatusFor<?php echo ucFirst($this->escape($key)); ?>}})">Enabled</div>
@ -252,15 +252,15 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
</ul>
</li>
<li data-state="/console/settings/domains?project={{router.params.project}}">
<?php if(!$customDomainsEnabled): ?>
<h2 style="display: none;">Custom Domains</h2>
<div class="box line margin-bottom">
<h3>Enable Custom Domains</h3>
<p>To enable <?php echo APP_NAME; ?>'s custom domain feature, you have to start your server instance with a public accessible domain name.</p>
<p>Start your <?php echo APP_NAME; ?> server container with the <b>_APP_DOMAIN_TARGET</b> environment variable set with a public accessible domain name that resolves to your <?php echo APP_NAME; ?> server setup.</p>
<p class="margin-bottom-no">The <?php echo APP_NAME; ?> server will use your target domain to validate new custom domains and will automatically generate SSL certificates for your new domains using Let'sencrypt Certbot.</p>
</div>
@ -312,7 +312,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<td data-title="">
<button class="link text-size-small" data-ls-if="true === {{domain.verification}}" data-ls-ui-trigger="dns-settings-{{domain.$id}}">DNS Settings</button>
<button class="link text-size-small" data-ls-if="true !== {{domain.verification}}" data-ls-ui-trigger="dns-settings-{{domain.$id}}">Verify Domain</button>
<div data-ui-modal class="modal box close" data-button-alias="none" data-open-event="dns-settings-{{domain.$id}}" xdata-close-event="dns-settings-close-{{domain.$id}}">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
@ -326,7 +326,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<ol class="bullets">
<li>
<p>Add a new CNAME record in your DNS providers settings to point your new subdomain to your <?php echo APP_NAME; ?> server with the following value:</p>
<div class="ide margin-bottom-small">
<pre class="line-numbers"><code class="prism language-javascript" data-prism><?php echo $this->print($customDomainsTarget, 'escape'); ?></code></pre>
</div>
@ -359,7 +359,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
<input type="hidden" name="domainId" data-ls-bind="{{domain.$id}}" />
<button class="margin-top-small">Confirm & Verify</button>
</form>
</li>
@ -428,7 +428,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<label for="name">Domain Name</label>
<input type="text" class="full-width" id="domain" name="domain" placeholder="appwrite.example.com" required autocomplete="off" title="Enter a valid domain name" pattern="^([a-zA-Z0-9][a-zA-Z0-9-_]*\.)*[a-zA-Z0-9]*[a-zA-Z0-9-_]*[[a-zA-Z0-9]+$" />
<hr />
<button type="submit">Create</button> &nbsp; <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
@ -463,6 +463,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
data-scope="console"
data-event="submit"
data-success="alert,trigger"
data-confirm="Are you sure you want to remove that user from the team?"
data-success-param-alert-text="Member Removed Successfully"
data-success-param-trigger-events="teams.deleteMembership"
data-failure="alert"
@ -474,7 +475,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<button class="danger">Leave</button>
</form>
<div data-ls-if="false === {{member.confirm}}" class="pull-end margin-end">
<form class="pull-end"
data-analytics
@ -553,7 +554,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<label for="email">Email</label>
<input name="email" id="email" type="email" autocomplete="email" required>
<label for="team-name">Name <small>(optional)</small></label>
<input name="name" id="team-name" type="text" autocomplete="name" maxlength="128" />

View file

@ -163,6 +163,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
data-validator="users.get"
required
maxlength="36"
pattern="^[a-zA-Z0-9][a-zA-Z0-9_-]{1,36}$"
id="userId"
name="userId" />
@ -310,6 +311,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
data-validator="teams.get"
required
maxlength="36"
pattern="^[a-zA-Z0-9][a-zA-Z0-9_-]{1,36}$"
id="teamId"
name="teamId" />
@ -327,7 +329,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<li data-state="/console/users/providers?project={{router.params.project}}">
<p data-ls-if="{{console-project.authLimit}} == 0" class="text-fade text-size-small margin-bottom pull-end">Unlimited Users <span class="link" data-ls-ui-trigger="project-update-auth-users-limit">Set Limit</a></p>
<p data-ls-if="{{console-project.authLimit}} != 0" class="text-fade text-size-small margin-bottom pull-end"><span data-ls-bind="{{console-project.authLimit|statsTotal}}"></span> Users allowed <span class="link" data-ls-ui-trigger="project-update-auth-users-limit">Change Limit</a></p>
<h2>Settings</h2>
<div data-ui-modal class="modal close" data-button-alias="none" data-open-event="project-update-auth-users-limit">
@ -335,8 +337,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<h1>Max Allowed Users</h1>
<form data-debug="1"
data-analytics
<form data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
@ -369,7 +370,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<ul class="tiles cell-3 margin-bottom-small">
<?php foreach($auth as $index => $method):
$key = $method['key'] ?? '';
$key = ucfirst($method['key'] ?? '');
$name = $method['name'] ?? '';
$icon = $method['icon'] ?? '';
$docs = $method['docs'] ?? '';
@ -394,17 +395,17 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
data-failure="alert"
data-failure-param-alert-text="Failed to update project auth status settings"
data-failure-param-alert-classname="error">
<input name="method" id="<?php echo $this->escape($key); ?>" type="hidden" autocomplete="off" value="<?php echo $this->escape($index); ?>">
<input name="method" id="auth<?php echo $this->escape($key); ?>" type="hidden" autocomplete="off" value="<?php echo $this->escape($index); ?>">
<?php if( !(in_array($key, ['usersAuthMagicURL', 'usersAuthInvites']) && !$smtpEnabled)): ?>
<input name="status" type="hidden" data-forms-switch data-ls-bind="{{console-project.<?php echo $this->escape($key); ?>}}" data-cast-to="boolean" class="pull-end" />
<input name="status" type="hidden" data-forms-switch data-ls-bind="{{console-project.auth<?php echo $this->escape($key); ?>}}" data-cast-to="boolean" class="pull-end" />
<?php endif; ?>
</form>
<?php endif; ?>
<img src="<?php echo $this->escape($icon); ?>?buster=<?php echo APP_CACHE_BUSTER; ?>" alt="Email/Password Logo" class="pull-start provider margin-end" />
<span class="text-size-small"><?php echo $this->escape($name); ?><?php if(!$enabled): ?> <spann class="text-fade text-size-xs">soon</span><?php endif; ?>
<?php if( in_array($key, ['usersAuthMagicURL', 'usersAuthInvites']) && !$smtpEnabled): ?>
<p class="margin-bottom-no text-one-liner text-size-small text-danger">
SMTP Disabled
@ -431,18 +432,18 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
data-scope="console">
<ul class="tiles cell-3 margin-bottom-small">
<?php foreach ($providers as $provider => $data):
if (isset($data['enabled']) && !$data['enabled']) {continue;}
// if (isset($data['mock']) && $data['mock']) {continue;}
if (isset($data['enabled']) && !$data['enabled']) {continue;}
if (isset($data['mock']) && $data['mock']) {continue;}
$sandbox = $data['sandbox'] ?? false;
$form = $data['form'] ?? false;
$name = $data['name'] ?? 'Unknown';
$beta = $data['beta'] ?? false;
?>
<li class="<?php echo (isset($data['enabled']) && !$data['enabled']) ? 'dev-feature' : ''; ?>">
<div data-ui-modal class="modal close" data-button-alias="none" data-open-event="provider-update-<?php echo $provider; ?>">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
?>
<li class="<?php echo (isset($data['enabled']) && !$data['enabled']) ? 'dev-feature' : ''; ?>">
<div data-ui-modal class="modal close" data-button-alias="none" data-open-event="provider-update-<?php echo $provider; ?>">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h1><?php echo $this->escape($name); ?> <?php if ($sandbox): ?>Sandbox<?php endif;?> OAuth2 Settings</h1>
<h1><?php echo $this->escape($name); ?> <?php if ($sandbox): ?>Sandbox<?php endif;?> OAuth2 Settings</h1>
<form
autocomplete="off"
@ -470,7 +471,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<?php if (!$form): ?>
<label for="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Appid">App ID</label>
<input name="appId" id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Appid" type="text" autocomplete="off" data-ls-bind="{{console-project.provider<?php echo $this->escape(ucfirst($provider)); ?>Appid}}">
<label for="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Secret">App Secret</label>
<input name="secret" data-forms-show-secret id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Secret" type="password" autocomplete="off" data-ls-bind="{{console-project.provider<?php echo $this->escape(ucfirst($provider)); ?>Secret}}">
<?php else: ?>
@ -524,5 +525,79 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
</ul>
</div>
</li>
<li data-state="/console/users/usage?project={{router.params.project}}">
<form
class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '90d'"
data-service="users.getUsage"
data-event="submit"
data-name="usage"
data-param-range="90d">
<button class="tick">90d</button>
</form>
<button class="tick pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} === '90d'" disabled>90d</button>
<form
class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '30d'"
data-service="users.getUsage"
data-event="submit"
data-name="usage">
<button class="tick">30d</button>
</form>
<button class="tick pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} === '30d'" disabled>30d</button>
<form
class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '24h'"
data-service="users.getUsage"
data-event="submit"
data-name="usage"
data-param-range="24h">
<button class="tick">24h</button>
</form>
<button class="tick pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} === '24h'" disabled>24h</button>
<h2>Usage</h2>
<div data-service="users.getUsage" data-event="load" data-name="usage">
<h3 class="margin-bottom-tiny">Users</h3>
<p class="text-fade">Count of users over time</p>
<div class="box margin-bottom-small">
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
<div class="chart margin-bottom-no">
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="Users=usersCount" data-show-y-axis="true" data-height="140" />
</div>
</div>
</div>
<ul class="chart-notes margin-bottom-large">
<li>Users <span data-ls-bind="({{usage.usersCount|statsGetLast|statsTotal}})"></span></li>
</ul>
<h3 class="margin-bottom-tiny">Operations</h3>
<p class="text-fade">Count of users create, read, update and delete operations over time</p>
<div class="box margin-bottom-small">
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
<div class="chart margin-bottom-no">
<input
type="hidden"
data-ls-bind="{{usage}}"
data-forms-chart="Created=usersCreate,Read=usersRead,Updated=usersUpdate,Deleted=usersDelete"
data-show-y-axis="true"
data-colors="create,read,update,delete"
data-height="140" />
</div>
</div>
</div>
<ul class="chart-notes margin-bottom-large crud">
<li>Created</li>
<li>Read</li>
<li>Updated</li>
<li>Deleted</li>
</ul>
</div>
</li>
</ul>
</div>

View file

@ -98,6 +98,7 @@
data-service="teams.deleteMembership"
data-event="submit"
data-success="alert,trigger"
data-confirm="Are you sure you want to remove that user from the team?"
data-success-param-alert-text="Member Removed Successfully"
data-success-param-trigger-events="teams.deleteMembership"
data-failure="alert"

View file

@ -106,12 +106,18 @@ class DatabaseV1 extends Worker
$dbForExternal = $this->getExternalDB($projectId);
$collectionId = $collection->getId();
$key = $attribute->getAttribute('key', '');
$status = $attribute->getAttribute('status', '');
// possible states at this point:
// - available: should not land in queue; controller flips these to 'deleting'
// - processing: hasn't finished creating
// - deleting: was available, in deletion queue for first time
// - failed: attribute was never created
// - stuck: attribute was available but cannot be removed
try {
if(!$dbForExternal->deleteAttribute($collectionId, $key) && $attribute->getAttribute('status') !== 'failed') {
if($status !== 'failed' && !$dbForExternal->deleteAttribute($collectionId, $key)) {
throw new Exception('Failed to delete Attribute');
}
$dbForInternal->deleteDocument('attributes', $attribute->getId());
} catch (\Throwable $th) {
Console::error($th->getMessage());
@ -214,12 +220,12 @@ class DatabaseV1 extends Worker
$collectionId = $collection->getId();
$key = $index->getAttribute('key');
$status = $index->getAttribute('status', '');
try {
if(!$dbForExternal->deleteIndex($collectionId, $key) && $index->getAttribute('status') !== 'failed') {
if($status !== 'failed' && !$dbForExternal->deleteIndex($collectionId, $key)) {
throw new Exception('Failed to delete index');
}
$dbForInternal->deleteDocument('indexes', $index->getId());
} catch (\Throwable $th) {
Console::error($th->getMessage());

View file

@ -11,7 +11,7 @@ use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\CLI\Console;
use Utopia\Audit\Audit;
require_once __DIR__.'/../init.php';
require_once __DIR__ . '/../init.php';
Console::title('Deletes V1 Worker');
Console::success(APP_NAME . ' deletes worker v1 has started' . "\n");
@ -31,12 +31,11 @@ class DeletesV1 extends Worker
{
$projectId = $this->args['projectId'] ?? '';
$type = $this->args['type'] ?? '';
switch (strval($type)) {
case DELETE_TYPE_DOCUMENT:
$document = $this->args['document'] ?? [];
$document = new Document($document);
$document = new Document($this->args['document'] ?? []);
switch ($document->getCollection()) {
case DELETE_TYPE_COLLECTIONS:
$this->deleteCollection($document, $projectId);
@ -75,8 +74,7 @@ class DeletesV1 extends Worker
break;
case DELETE_TYPE_REALTIME:
//$this->deleteRealtimeUsage($this->args['timestamp']);
//TODO: implement this
$this->deleteRealtimeUsage($this->args['timestamp']);
break;
case DELETE_TYPE_CERTIFICATES:
@ -123,12 +121,10 @@ class DeletesV1 extends Worker
* @param int $timestamp1d
* @param int $timestamp30m
*/
protected function deleteUsageStats(int $timestamp1d, int $timestamp30m) {
$this->deleteForProjectIds(function($projectId) use ($timestamp1d, $timestamp30m) {
if (!($dbForInternal = $this->getInternalDB($projectId))) {
throw new Exception('Failed to get projectDB for project '.$projectId);
}
protected function deleteUsageStats(int $timestamp1d, int $timestamp30m)
{
$this->deleteForProjectIds(function (string $projectId) use ($timestamp1d, $timestamp30m) {
$dbForInternal = $this->getInternalDB($projectId);
// Delete Usage stats
$this->deleteByGroup('stats', [
new Query('time', Query::TYPE_LESSER, [$timestamp1d]),
@ -141,7 +137,7 @@ class DeletesV1 extends Worker
], $dbForInternal);
});
}
/**
* @param Document $document teams document
* @param string $projectId
@ -162,13 +158,14 @@ class DeletesV1 extends Worker
protected function deleteProject(Document $document): void
{
$projectId = $document->getId();
// Delete all DBs
$this->getExternalDB($projectId)->delete();
$this->getInternalDB($projectId)->delete();
// Delete all storage directories
$uploads = new Local(APP_STORAGE_UPLOADS.'/app-'.$document->getId());
$cache = new Local(APP_STORAGE_CACHE.'/app-'.$document->getId());
$uploads = new Local(APP_STORAGE_UPLOADS . '/app-' . $document->getId());
$cache = new Local(APP_STORAGE_CACHE . '/app-' . $document->getId());
$uploads->delete($uploads->getRoot(), true);
$cache->delete($cache->getRoot(), true);
@ -186,12 +183,12 @@ class DeletesV1 extends Worker
// Delete Memberships and decrement team membership counts
$this->deleteByGroup('memberships', [
new Query('userId', Query::TYPE_EQUAL, [$userId])
], $this->getInternalDB($projectId), function(Document $document) use ($projectId, $userId) {
], $this->getInternalDB($projectId), function (Document $document) use ($projectId) {
if ($document->getAttribute('confirm')) { // Count only confirmed members
$teamId = $document->getAttribute('teamId');
$team = $this->getInternalDB($projectId)->getDocument('teams', $teamId);
if(!$team->isEmpty()) {
if (!$team->isEmpty()) {
$team = $this->getInternalDB($projectId)->updateDocument('teams', $teamId, new Document(\array_merge($team->getArrayCopy(), [
'sum' => \max($team->getAttribute('sum', 0) - 1, 0), // Ensure that sum >= 0
])));
@ -205,11 +202,8 @@ class DeletesV1 extends Worker
*/
protected function deleteExecutionLogs(int $timestamp): void
{
$this->deleteForProjectIds(function(string $projectId) use ($timestamp) {
if (!($dbForInternal = $this->getInternalDB($projectId))) {
throw new Exception('Failed to get projectDB for project '.$projectId);
}
$this->deleteForProjectIds(function (string $projectId) use ($timestamp) {
$dbForInternal = $this->getInternalDB($projectId);
// Delete Executions
$this->deleteByGroup('executions', [
new Query('dateCreated', Query::TYPE_LESSER, [$timestamp])
@ -217,18 +211,33 @@ class DeletesV1 extends Worker
});
}
/**
* @param int $timestamp
*/
protected function deleteRealtimeUsage(int $timestamp): void
{
$this->deleteForProjectIds(function (string $projectId) use ($timestamp) {
$dbForInternal = $this->getInternalDB($projectId);
// Delete Dead Realtime Logs
$this->deleteByGroup('realtime', [
new Query('timestamp', Query::TYPE_LESSER, [$timestamp])
], $dbForInternal);
});
}
/**
* @param int $timestamp
*/
protected function deleteAbuseLogs(int $timestamp): void
{
if($timestamp == 0) {
if ($timestamp == 0) {
throw new Exception('Failed to delete audit logs. No timestamp provided');
}
$this->deleteForProjectIds(function($projectId) use ($timestamp){
$timeLimit = new TimeLimit("", 0, 1, $this->getInternalDB($projectId));
$abuse = new Abuse($timeLimit);
$this->deleteForProjectIds(function (string $projectId) use ($timestamp) {
$dbForInternal = $this->getInternalDB($projectId);
$timeLimit = new TimeLimit("", 0, 1, $dbForInternal);
$abuse = new Abuse($timeLimit);
$status = $abuse->cleanup($timestamp);
if (!$status) {
@ -242,11 +251,12 @@ class DeletesV1 extends Worker
*/
protected function deleteAuditLogs(int $timestamp): void
{
if($timestamp == 0) {
if ($timestamp == 0) {
throw new Exception('Failed to delete audit logs. No timestamp provided');
}
$this->deleteForProjectIds(function($projectId) use ($timestamp){
$audit = new Audit($this->getInternalDB($projectId));
$this->deleteForProjectIds(function (string $projectId) use ($timestamp) {
$dbForInternal = $this->getInternalDB($projectId);
$audit = new Audit($dbForInternal);
$status = $audit->cleanup($timestamp);
if (!$status) {
throw new Exception('Failed to delete Audit logs for project' . $projectId);
@ -261,12 +271,12 @@ class DeletesV1 extends Worker
protected function deleteFunction(Document $document, string $projectId): void
{
$dbForInternal = $this->getInternalDB($projectId);
$device = new Local(APP_STORAGE_FUNCTIONS.'/app-'.$projectId);
$device = new Local(APP_STORAGE_FUNCTIONS . '/app-' . $projectId);
// Delete Tags
$this->deleteByGroup('tags', [
new Query('functionId', Query::TYPE_EQUAL, [$document->getId()])
], $dbForInternal, function(Document $document) use ($device) {
], $dbForInternal, function (Document $document) use ($device) {
if ($device->delete($document->getAttribute('path', ''))) {
Console::success('Delete code tag: ' . $document->getAttribute('path', ''));
@ -293,8 +303,8 @@ class DeletesV1 extends Worker
{
Authorization::disable();
if($database->deleteDocument($document->getCollection(), $document->getId())) {
Console::success('Deleted document "'.$document->getId().'" successfully');
if ($database->deleteDocument($document->getCollection(), $document->getId())) {
Console::success('Deleted document "' . $document->getId() . '" successfully');
if (is_callable($callback)) {
$callback($document);
@ -322,16 +332,15 @@ class DeletesV1 extends Worker
$executionStart = \microtime(true);
while($sum === $limit) {
while ($sum === $limit) {
Authorization::disable();
$projects = $this->getConsoleDB()->find('projects', [], $limit, ($chunk * $limit));
Authorization::reset();
$chunk++;
$projectIds = array_map (function ($project) {
return $project->getId();
}, $projects);
/** @var string[] $projectIds */
$projectIds = array_map(fn(Document $project) => $project->getId(), $projects);
$sum = count($projects);
@ -362,7 +371,7 @@ class DeletesV1 extends Worker
$executionStart = \microtime(true);
while($sum === $limit) {
while ($sum === $limit) {
$chunk++;
Authorization::disable();
@ -408,7 +417,7 @@ class DeletesV1 extends Worker
{
$bucketId = $document->getId();
$this->deleteByGroup('files',[
$this->deleteByGroup($bucketId . '_files',[
new Query('bucketId', Query::TYPE_EQUAL, [$bucketId])
], $this->getExternalDB($projectId));

View file

@ -41,11 +41,11 @@
"utopia-php/framework": "0.19.*",
"utopia-php/abuse": "0.6.*",
"utopia-php/analytics": "0.2.*",
"utopia-php/audit": "0.6.*",
"utopia-php/audit": "0.7.*",
"utopia-php/cache": "0.4.*",
"utopia-php/cli": "0.11.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.10.*",
"utopia-php/database": "0.12.*",
"utopia-php/locale": "0.4.*",
"utopia-php/orchestration": "0.2.*",
"utopia-php/registry": "0.5.*",

74
composer.lock generated
View file

@ -1591,16 +1591,16 @@
},
{
"name": "symfony/deprecation-contracts",
"version": "v2.4.0",
"version": "v2.5.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627"
"reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5f38c8804a9e97d23e0c8d63341088cd8a22d627",
"reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/6f981ee24cf69ee7ce9736146d1c57c2780598a8",
"reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8",
"shasum": ""
},
"require": {
@ -1609,7 +1609,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "2.4-dev"
"dev-main": "2.5-dev"
},
"thanks": {
"name": "symfony/contracts",
@ -1638,7 +1638,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v2.4.0"
"source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.0"
},
"funding": [
{
@ -1654,7 +1654,7 @@
"type": "tidelift"
}
],
"time": "2021-03-23T23:28:01+00:00"
"time": "2021-07-12T14:48:14+00:00"
},
{
"name": "symfony/polyfill-ctype",
@ -1928,22 +1928,22 @@
},
{
"name": "utopia-php/audit",
"version": "0.6.3",
"version": "0.7.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/audit.git",
"reference": "d79b467fbc7d03e5e02f12cdeb08761507a60ca0"
"reference": "485cdd2354db7eb8f7aa74bbe39c39b583e99c04"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/d79b467fbc7d03e5e02f12cdeb08761507a60ca0",
"reference": "d79b467fbc7d03e5e02f12cdeb08761507a60ca0",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/485cdd2354db7eb8f7aa74bbe39c39b583e99c04",
"reference": "485cdd2354db7eb8f7aa74bbe39c39b583e99c04",
"shasum": ""
},
"require": {
"ext-pdo": "*",
"php": ">=7.4",
"utopia-php/database": ">=0.6 <1.0"
"utopia-php/database": ">=0.11 <1.0"
},
"require-dev": {
"phpunit/phpunit": "^9.3",
@ -1975,9 +1975,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/audit/issues",
"source": "https://github.com/utopia-php/audit/tree/0.6.3"
"source": "https://github.com/utopia-php/audit/tree/0.7.0"
},
"time": "2021-08-16T18:49:55+00:00"
"time": "2021-11-17T17:23:42+00:00"
},
{
"name": "utopia-php/cache",
@ -4102,16 +4102,16 @@
},
{
"name": "phpunit/php-code-coverage",
"version": "9.2.8",
"version": "9.2.9",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "cf04e88a2e3c56fc1a65488afd493325b4c1bc3e"
"reference": "f301eb1453c9e7a1bc912ee8b0ea9db22c60223b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/cf04e88a2e3c56fc1a65488afd493325b4c1bc3e",
"reference": "cf04e88a2e3c56fc1a65488afd493325b4c1bc3e",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f301eb1453c9e7a1bc912ee8b0ea9db22c60223b",
"reference": "f301eb1453c9e7a1bc912ee8b0ea9db22c60223b",
"shasum": ""
},
"require": {
@ -4167,7 +4167,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.8"
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.9"
},
"funding": [
{
@ -4175,7 +4175,7 @@
"type": "github"
}
],
"time": "2021-10-30T08:01:38+00:00"
"time": "2021-11-19T15:21:02+00:00"
},
{
"name": "phpunit/php-file-iterator",
@ -5422,6 +5422,7 @@
"type": "github"
}
],
"abandoned": true,
"time": "2020-09-28T06:45:17+00:00"
},
{
@ -5587,16 +5588,16 @@
},
{
"name": "symfony/console",
"version": "v5.3.10",
"version": "v5.3.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "d4e409d9fbcfbf71af0e5a940abb7b0b4bad0bd3"
"reference": "3e7ab8f5905058984899b05a4648096f558bfeba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/d4e409d9fbcfbf71af0e5a940abb7b0b4bad0bd3",
"reference": "d4e409d9fbcfbf71af0e5a940abb7b0b4bad0bd3",
"url": "https://api.github.com/repos/symfony/console/zipball/3e7ab8f5905058984899b05a4648096f558bfeba",
"reference": "3e7ab8f5905058984899b05a4648096f558bfeba",
"shasum": ""
},
"require": {
@ -5609,7 +5610,6 @@
"symfony/string": "^5.1"
},
"conflict": {
"psr/log": ">=3",
"symfony/dependency-injection": "<4.4",
"symfony/dotenv": "<5.1",
"symfony/event-dispatcher": "<4.4",
@ -5666,7 +5666,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v5.3.10"
"source": "https://github.com/symfony/console/tree/v5.3.11"
},
"funding": [
{
@ -5682,7 +5682,7 @@
"type": "tidelift"
}
],
"time": "2021-10-26T09:30:15+00:00"
"time": "2021-11-21T19:41:05+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
@ -6010,21 +6010,25 @@
},
{
"name": "symfony/service-contracts",
"version": "v2.4.0",
"version": "v2.5.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
"reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb"
"reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb",
"reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc",
"reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"psr/container": "^1.1"
"psr/container": "^1.1",
"symfony/deprecation-contracts": "^2.1"
},
"conflict": {
"ext-psr": "<1.1|>=2"
},
"suggest": {
"symfony/service-implementation": ""
@ -6032,7 +6036,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "2.4-dev"
"dev-main": "2.5-dev"
},
"thanks": {
"name": "symfony/contracts",
@ -6069,7 +6073,7 @@
"standards"
],
"support": {
"source": "https://github.com/symfony/service-contracts/tree/v2.4.0"
"source": "https://github.com/symfony/service-contracts/tree/v2.5.0"
},
"funding": [
{
@ -6085,7 +6089,7 @@
"type": "tidelift"
}
],
"time": "2021-04-01T10:43:52+00:00"
"time": "2021-11-04T16:48:04+00:00"
},
{
"name": "symfony/string",

View file

@ -0,0 +1 @@
Get the document activity logs list by its unique ID.

View file

@ -58,14 +58,16 @@ For **Windows** add your app <u>name</u> and <u>package name</u>, Your package n
```dart
import 'package:appwrite/appwrite.dart';
Client client = Client();
void main() {
Client client = Client();
client
.setEndpoint('https://localhost/v1') // Your Appwrite Endpoint
.setProject('5e8cf4f46b5e8') // Your project ID
.setSelfSigned() // Use only on dev mode with a self-signed SSL cert
;
client
.setEndpoint('https://localhost/v1') // Your Appwrite Endpoint
.setProject('5e8cf4f46b5e8') // Your project ID
.setSelfSigned() // Use only on dev mode with a self-signed SSL cert
;
}
```
Before starting to send any API calls to your new Appwrite instance, make sure your Android or iOS emulators has network access to the Appwrite server hostname or IP address.
@ -91,25 +93,28 @@ Response user = await account
```dart
import 'package:appwrite/appwrite.dart';
Client client = Client();
void main() {
Client client = Client();
client
.setEndpoint('https://localhost/v1') // Your Appwrite Endpoint
.setProject('5e8cf4f46b5e8') // Your project ID
.setSelfSigned() // Use only on dev mode with a self-signed SSL cert
;
client
.setEndpoint('https://localhost/v1') // Your Appwrite Endpoint
.setProject('5e8cf4f46b5e8') // Your project ID
.setSelfSigned() // Use only on dev mode with a self-signed SSL cert
;
// Register User
Account account = Account(client);
// Register User
Account account = Account(client);
Response user = await account
.create(
email: 'me@appwrite.io',
password: 'password',
name: 'My Name'
);
Response user = await account
.create(
email: 'me@appwrite.io',
password: 'password',
name: 'My Name'
);
}
```
### Error Handling

View file

@ -13,6 +13,7 @@ const configApp = {
mainFile: 'app.js',
src: [
'public/scripts/dependencies/litespeed.js',
'public/scripts/dependencies/alpine.js',
'public/scripts/init.js',
@ -59,9 +60,11 @@ const configApp = {
'public/scripts/views/forms/oauth-apple.js',
'public/scripts/views/forms/password-meter.js',
'public/scripts/views/forms/pell.js',
'public/scripts/views/forms/required.js',
'public/scripts/views/forms/remove.js',
'public/scripts/views/forms/run.js',
'public/scripts/views/forms/select-all.js',
'public/scripts/views/forms/selected.js',
'public/scripts/views/forms/show-secret.js',
'public/scripts/views/forms/switch.js',
'public/scripts/views/forms/tags.js',

File diff suppressed because one or more lines are too long

View file

@ -23,7 +23,9 @@ const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/account/email';let payload={};if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),createJWT:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/jwt';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),getLogs:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/logs';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updateName:(name)=>__awaiter(this,void 0,void 0,function*(){if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');}
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),createJWT:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/jwt';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),getLogs:(limit,offset)=>__awaiter(this,void 0,void 0,function*(){let path='/account/logs';let payload={};if(typeof limit!=='undefined'){payload['limit']=limit;}
if(typeof offset!=='undefined'){payload['offset']=offset;}
const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updateName:(name)=>__awaiter(this,void 0,void 0,function*(){if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');}
let path='/account/name';let payload={};if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),updatePassword:(password,oldPassword)=>__awaiter(this,void 0,void 0,function*(){if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/account/password';let payload={};if(typeof password!=='undefined'){payload['password']=password;}
@ -223,7 +225,11 @@ if(typeof read!=='undefined'){payload['read']=read;}
if(typeof write!=='undefined'){payload['write']=write;}
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),deleteDocument:(collectionId,documentId)=>__awaiter(this,void 0,void 0,function*(){if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
if(typeof documentId==='undefined'){throw new AppwriteException('Missing required parameter: "documentId"');}
let path='/database/collections/{collectionId}/documents/{documentId}'.replace('{collectionId}',collectionId).replace('{documentId}',documentId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),listIndexes:(collectionId)=>__awaiter(this,void 0,void 0,function*(){if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
let path='/database/collections/{collectionId}/documents/{documentId}'.replace('{collectionId}',collectionId).replace('{documentId}',documentId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),listDocumentLogs:(collectionId,documentId,limit,offset)=>__awaiter(this,void 0,void 0,function*(){if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
if(typeof documentId==='undefined'){throw new AppwriteException('Missing required parameter: "documentId"');}
let path='/database/collections/{collectionId}/documents/{documentId}/logs'.replace('{collectionId}',collectionId).replace('{documentId}',documentId);let payload={};if(typeof limit!=='undefined'){payload['limit']=limit;}
if(typeof offset!=='undefined'){payload['offset']=offset;}
const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),listIndexes:(collectionId)=>__awaiter(this,void 0,void 0,function*(){if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
let path='/database/collections/{collectionId}/indexes'.replace('{collectionId}',collectionId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createIndex:(collectionId,indexId,type,attributes,orders)=>__awaiter(this,void 0,void 0,function*(){if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
if(typeof indexId==='undefined'){throw new AppwriteException('Missing required parameter: "indexId"');}
if(typeof type==='undefined'){throw new AppwriteException('Missing required parameter: "type"');}
@ -236,8 +242,10 @@ const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{
if(typeof indexId==='undefined'){throw new AppwriteException('Missing required parameter: "indexId"');}
let path='/database/collections/{collectionId}/indexes/{indexId}'.replace('{collectionId}',collectionId).replace('{indexId}',indexId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),deleteIndex:(collectionId,indexId)=>__awaiter(this,void 0,void 0,function*(){if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
if(typeof indexId==='undefined'){throw new AppwriteException('Missing required parameter: "indexId"');}
let path='/database/collections/{collectionId}/indexes/{indexId}'.replace('{collectionId}',collectionId).replace('{indexId}',indexId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),listCollectionLogs:(collectionId)=>__awaiter(this,void 0,void 0,function*(){if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
let path='/database/collections/{collectionId}/logs'.replace('{collectionId}',collectionId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getUsage:(range)=>__awaiter(this,void 0,void 0,function*(){let path='/database/usage';let payload={};if(typeof range!=='undefined'){payload['range']=range;}
let path='/database/collections/{collectionId}/indexes/{indexId}'.replace('{collectionId}',collectionId).replace('{indexId}',indexId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),listCollectionLogs:(collectionId,limit,offset)=>__awaiter(this,void 0,void 0,function*(){if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
let path='/database/collections/{collectionId}/logs'.replace('{collectionId}',collectionId);let payload={};if(typeof limit!=='undefined'){payload['limit']=limit;}
if(typeof offset!=='undefined'){payload['offset']=offset;}
const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getUsage:(range)=>__awaiter(this,void 0,void 0,function*(){let path='/database/usage';let payload={};if(typeof range!=='undefined'){payload['range']=range;}
const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getCollectionUsage:(collectionId,range)=>__awaiter(this,void 0,void 0,function*(){if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
let path='/database/{collectionId}/usage'.replace('{collectionId}',collectionId);let payload={};if(typeof range!=='undefined'){payload['range']=range;}
const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);})};this.functions={list:(search,limit,offset,cursor,cursorDirection,orderType)=>__awaiter(this,void 0,void 0,function*(){let path='/functions';let payload={};if(typeof search!=='undefined'){payload['search']=search;}
@ -574,8 +582,10 @@ let path='/users/{userId}'.replace('{userId}',userId);let payload={};const uri=n
let path='/users/{userId}'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),updateEmail:(userId,email)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
let path='/users/{userId}/email'.replace('{userId}',userId);let payload={};if(typeof email!=='undefined'){payload['email']=email;}
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),getLogs:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
let path='/users/{userId}/logs'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updateName:(userId,name)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),getLogs:(userId,limit,offset)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
let path='/users/{userId}/logs'.replace('{userId}',userId);let payload={};if(typeof limit!=='undefined'){payload['limit']=limit;}
if(typeof offset!=='undefined'){payload['offset']=offset;}
const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updateName:(userId,name)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');}
let path='/users/{userId}/name'.replace('{userId}',userId);let payload={};if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),updatePassword:(userId,password)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -321,12 +321,20 @@
* Get currently logged in user list of latest security activity logs. Each
* log returns user IP address, location and date and time of log.
*
* @param {number} limit
* @param {number} offset
* @throws {AppwriteException}
* @returns {Promise}
*/
getLogs: () => __awaiter(this, void 0, void 0, function* () {
getLogs: (limit, offset) => __awaiter(this, void 0, void 0, function* () {
let path = '/account/logs';
let payload = {};
if (typeof limit !== 'undefined') {
payload['limit'] = limit;
}
if (typeof offset !== 'undefined') {
payload['offset'] = offset;
}
const uri = new URL(this.config.endpoint + path);
return yield this.call('get', uri, {
'content-type': 'application/json',
@ -1910,6 +1918,38 @@
'content-type': 'application/json',
}, payload);
}),
/**
* List Document Logs
*
* Get the document activity logs list by its unique ID.
*
* @param {string} collectionId
* @param {string} documentId
* @param {number} limit
* @param {number} offset
* @throws {AppwriteException}
* @returns {Promise}
*/
listDocumentLogs: (collectionId, documentId, limit, offset) => __awaiter(this, void 0, void 0, function* () {
if (typeof collectionId === 'undefined') {
throw new AppwriteException('Missing required parameter: "collectionId"');
}
if (typeof documentId === 'undefined') {
throw new AppwriteException('Missing required parameter: "documentId"');
}
let path = '/database/collections/{collectionId}/documents/{documentId}/logs'.replace('{collectionId}', collectionId).replace('{documentId}', documentId);
let payload = {};
if (typeof limit !== 'undefined') {
payload['limit'] = limit;
}
if (typeof offset !== 'undefined') {
payload['offset'] = offset;
}
const uri = new URL(this.config.endpoint + path);
return yield this.call('get', uri, {
'content-type': 'application/json',
}, payload);
}),
/**
* List Indexes
*
@ -2025,15 +2065,23 @@
* Get the collection activity logs list by its unique ID.
*
* @param {string} collectionId
* @param {number} limit
* @param {number} offset
* @throws {AppwriteException}
* @returns {Promise}
*/
listCollectionLogs: (collectionId) => __awaiter(this, void 0, void 0, function* () {
listCollectionLogs: (collectionId, limit, offset) => __awaiter(this, void 0, void 0, function* () {
if (typeof collectionId === 'undefined') {
throw new AppwriteException('Missing required parameter: "collectionId"');
}
let path = '/database/collections/{collectionId}/logs'.replace('{collectionId}', collectionId);
let payload = {};
if (typeof limit !== 'undefined') {
payload['limit'] = limit;
}
if (typeof offset !== 'undefined') {
payload['offset'] = offset;
}
const uri = new URL(this.config.endpoint + path);
return yield this.call('get', uri, {
'content-type': 'application/json',
@ -4935,15 +4983,23 @@
* Get the user activity logs list by its unique ID.
*
* @param {string} userId
* @param {number} limit
* @param {number} offset
* @throws {AppwriteException}
* @returns {Promise}
*/
getLogs: (userId) => __awaiter(this, void 0, void 0, function* () {
getLogs: (userId, limit, offset) => __awaiter(this, void 0, void 0, function* () {
if (typeof userId === 'undefined') {
throw new AppwriteException('Missing required parameter: "userId"');
}
let path = '/users/{userId}/logs'.replace('{userId}', userId);
let payload = {};
if (typeof limit !== 'undefined') {
payload['limit'] = limit;
}
if (typeof offset !== 'undefined') {
payload['offset'] = offset;
}
const uri = new URL(this.config.endpoint + path);
return yield this.call('get', uri, {
'content-type': 'application/json',

View file

@ -1,16 +1,16 @@
window.ls.filter
.add("avatar", function($value, element) {
.add("avatar", function ($value, element) {
if (!$value) {
return "";
}
let size = element.dataset["size"] || 80;
let name = $value.name || $value || "";
name = (typeof name !== 'string') ? '--' : name;
return def =
"/v1/avatars/initials?project=console"+
"/v1/avatars/initials?project=console" +
"&name=" +
encodeURIComponent(name) +
"&width=" +
@ -18,26 +18,26 @@ window.ls.filter
"&height=" +
size;
})
.add("selectedCollection", function($value, router) {
.add("selectedCollection", function ($value, router) {
return $value === router.params.collectionId ? "selected" : "";
})
.add("selectedDocument", function($value, router) {
.add("selectedDocument", function ($value, router) {
return $value === router.params.documentId ? "selected" : "";
})
.add("localeString", function($value) {
.add("localeString", function ($value) {
$value = parseInt($value);
return !Number.isNaN($value) ? $value.toLocaleString() : "";
})
.add("date", function($value, date) {
.add("date", function ($value, date) {
return date.format("Y-m-d", $value);
})
.add("dateTime", function($value, date) {
.add("dateTime", function ($value, date) {
return date.format("Y-m-d H:i", $value);
})
.add("dateText", function($value, date) {
.add("dateText", function ($value, date) {
return date.format("d M Y", $value);
})
.add("timeSince", function($value) {
.add("timeSince", function ($value) {
$value = $value * 1000;
let seconds = Math.floor((Date.now() - $value) / 1000);
@ -50,7 +50,7 @@ window.ls.filter
}
let value = seconds;
if (seconds >= 31536000) {
value = Math.floor(seconds / 31536000);
unit = "year";
@ -71,10 +71,10 @@ window.ls.filter
if (value != 1) {
unit = unit + "s";
}
return value + " " + unit + " " + direction;
})
.add("ms2hum", function($value) {
.add("ms2hum", function ($value) {
let temp = $value;
const years = Math.floor(temp / 31536000),
days = Math.floor((temp %= 31536000) / 86400),
@ -95,37 +95,37 @@ window.ls.filter
return "< 1s";
})
.add("seconds2hum", function($value) {
.add("seconds2hum", function ($value) {
var seconds = ($value).toFixed(3);
var seconds = ($value).toFixed(3);
var minutes = ($value / (60)).toFixed(1);
var minutes = ($value / (60)).toFixed(1);
var hours = ($value / (60 * 60)).toFixed(1);
var hours = ($value / (60 * 60)).toFixed(1);
var days = ($value / (60 * 60 * 24)).toFixed(1);
var days = ($value / (60 * 60 * 24)).toFixed(1);
if (seconds < 60) {
return seconds + "s";
} else if (minutes < 60) {
return minutes + "m";
} else if (hours < 24) {
return hours + "h";
} else {
return days + "d"
}
if (seconds < 60) {
return seconds + "s";
} else if (minutes < 60) {
return minutes + "m";
} else if (hours < 24) {
return hours + "h";
} else {
return days + "d"
}
})
.add("markdown", function($value, markdown) {
.add("markdown", function ($value, markdown) {
return markdown.render($value);
})
.add("pageCurrent", function($value, env) {
.add("pageCurrent", function ($value, env) {
return Math.ceil(parseInt($value || 0) / env.PAGING_LIMIT) + 1;
})
.add("pageTotal", function($value, env) {
.add("pageTotal", function ($value, env) {
let total = Math.ceil(parseInt($value || 0) / env.PAGING_LIMIT);
return total ? total : 1;
})
.add("humanFileSize", function($value) {
.add("humanFileSize", function ($value) {
if (!$value) {
return 0;
}
@ -146,7 +146,7 @@ window.ls.filter
return $value.toFixed(1);
})
.add("humanFileUnit", function($value) {
.add("humanFileUnit", function ($value) {
if (!$value) {
return '';
}
@ -183,95 +183,102 @@ window.ls.filter
return $value[$value.length - 1].value;
})
.add("isEmpty", function($value) {
.add("statsGetLast", function ($value) {
if (!$value || $value.length < 1) {
return 0;
}
return $value[$value.length - 1].value;
})
.add("isEmpty", function ($value) {
return (!!$value);
})
.add("isEmptyObject", function($value) {
.add("isEmptyObject", function ($value) {
return ((Object.keys($value).length === 0 && $value.constructor === Object) || $value.length === 0)
})
.add("activeDomainsCount", function($value) {
.add("activeDomainsCount", function ($value) {
let result = [];
if(Array.isArray($value)) {
result = $value.filter(function(node) {
if (Array.isArray($value)) {
result = $value.filter(function (node) {
return (node.verification && node.certificateId);
});
}
return result.length;
})
.add("documentAction", function(container) {
.add("documentAction", function (container) {
let collection = container.get('project-collection');
let document = container.get('project-document');
if(collection && document && !document.$id) {
if (collection && document && !document.$id) {
return 'database.createDocument';
}
return 'database.updateDocument';
})
.add("documentSuccess", function(container) {
.add("documentSuccess", function (container) {
let document = container.get('project-document');
if(document && !document.$id) {
if (document && !document.$id) {
return ',redirect';
}
return '';
})
.add("firstElement", function($value) {
if($value && $value[0]) {
.add("firstElement", function ($value) {
if ($value && $value[0]) {
return $value[0];
}
return $value;
})
.add("platformsLimit", function($value) {
.add("platformsLimit", function ($value) {
return $value;
})
.add("limit", function($value) {
.add("limit", function ($value) {
let postfix = ($value.length >= 50) ? '...' : '';
return $value.substring(0, 50) + postfix;
;
})
.add("arraySentence", function($value) {
if(!Array.isArray($value)) {
.add("arraySentence", function ($value) {
if (!Array.isArray($value)) {
return '';
}
return $value.join(", ").replace(/,\s([^,]+)$/, ' and $1');
})
.add("runtimeName", function($value, env) {
if(env && env.RUNTIMES && env.RUNTIMES[$value]) {
.add("runtimeName", function ($value, env) {
if (env && env.RUNTIMES && env.RUNTIMES[$value]) {
return env.RUNTIMES[$value].name;
}
return '';
})
.add("runtimeLogo", function($value, env) {
if(env && env.RUNTIMES && env.RUNTIMES[$value]) {
.add("runtimeLogo", function ($value, env) {
if (env && env.RUNTIMES && env.RUNTIMES[$value]) {
return env.RUNTIMES[$value].logo;
}
return '';
})
.add("runtimeVersion", function($value, env) {
if(env && env.RUNTIMES && env.RUNTIMES[$value]) {
.add("runtimeVersion", function ($value, env) {
if (env && env.RUNTIMES && env.RUNTIMES[$value]) {
return env.RUNTIMES[$value].version;
}
return '';
})
.add("indexAttributes", function($value) {
.add("indexAttributes", function ($value) {
let output = '';
for(let i = 0; i < $value.attributes.length; i++) {
for (let i = 0; i < $value.attributes.length; i++) {
output += $value.attributes[i] + ' (' + $value.orders[i] + '), '
}
return output.slice(0, -2);
})
.add("collectionAttributes", function($value) {
if(!Array.isArray($value)) {
.add("collectionAttributes", function ($value) {
if (!Array.isArray($value)) {
return [];
}
@ -281,17 +288,23 @@ window.ls.filter
return $value;
})
.add("documentAttribute", function($value, attribute) {
if($value[attribute.key]) {
.add("documentAttribute", function ($value, attribute) {
if (attribute.key in $value) {
return $value[attribute.key];
}
return null;
})
.add("accessProject", function($value, router) {
.add("accessProject", function ($value, router) {
return ($value && $value.hasOwnProperty(router.params.project)) ? $value[router.params.project] : 0;
})
;
.add("first", function ($value) {
return $value[0].$id;
})
.add("last", function ($value) {
return $value[$value.length - 1].$id;
})
;
function abbreviate(number, maxPlaces, forcePlaces, forceLetter) {
number = Number(number);

View file

@ -123,3 +123,66 @@ window.addEventListener("load", async () => {
}
});
window.formValidation = (form, fields) => {
const elements = form.elements;
const actionHandler = (action, attribute) => {
switch (action) {
case "disable":
elements[attribute].setAttribute("disabled", true);
elements[attribute].dispatchEvent(new Event('change'));
break;
case "enable":
elements[attribute].removeAttribute("disabled");
elements[attribute].dispatchEvent(new Event('change'));
break;
case "unvalue":
elements[attribute].value = "";
break;
case "check":
elements[attribute].value = "true";
break;
case "uncheck":
elements[attribute].value = "false";
break;
}
};
for (const field in fields) {
for (const attribute in fields[field]) {
const attr = fields[field][attribute];
if (Array.isArray(attr)) {
attr.forEach(action => {
if (elements[field].value === "true") {
actionHandler(action, attribute);
}
})
} else {
const condition = attr.if.some(c => {
return elements[c].value === "true";
});
if (condition) {
for (const thenAction in attr.then) {
attr.then[thenAction].forEach(action => {
actionHandler(action, thenAction);
});
}
} else {
for (const elseAction in attr.else) {
attr.else[elseAction].forEach(action => {
actionHandler(action, elseAction);
});
}
}
}
}
}
form.addEventListener("reset", () => {
for (const key in fields) {
if (Object.hasOwnProperty.call(fields, key)) {
const element = form.elements[key];
element.setAttribute("value", "");
element.removeAttribute("disabled");
element.dispatchEvent(new Event("change"));
}
}
});
};

View file

@ -113,6 +113,13 @@ window.ls.router
scope: "console",
project: true
})
.add("/console/database/usage", {
template: function(window) {
return window.location.pathname + window.location.search + '&version=' + APP_ENV.CACHEBUSTER;
},
scope: "console",
project: true
})
.add("/console/database/collection", {
template: function(window) {
return window.location.pathname + window.location.search + '&version=' + APP_ENV.CACHEBUSTER;

View file

@ -13,11 +13,20 @@
case 'integer':
value = parseInt(value);
break;
case 'float':
value = parseFloat(parseFloat(value).toFixed(2));
break;
case 'numeric':
value = Number(value);
break;
case 'float':
value = parseFloat(value);
break;
case 'string':
value = value.toString();
if (value.length === 0) {
value = null;
}
break;
case 'json':
value = (value) ? JSON.parse(value) : [];
@ -105,7 +114,7 @@
json[name] = element.value;
}
}
json[name] = cast(json[name], castTo); // Apply casting
}
}

View file

@ -1,9 +1,9 @@
(function(window) {
(function (window) {
"use strict";
window.ls.container.get("view").add({
selector: "data-forms-chart",
controller: function(element, container, date, document) {
controller: function (element, container, date, document) {
let wrapper = document.createElement("div");
let child = document.createElement("canvas");
let sources = element.getAttribute('data-forms-chart');
@ -19,7 +19,7 @@
element.parentNode.insertBefore(wrapper, element.nextSibling);
wrapper.classList.add('content');
child.width = width;
child.height = height;
@ -29,7 +29,7 @@
let chart = null;
let check = function() {
let check = function () {
let config = {
type: "line",
@ -41,7 +41,7 @@
responsive: true,
hover: {
mode: "nearest",
intersect: true
intersect: false
},
scales: {
x: {
@ -52,6 +52,7 @@
min: 0,
ticks: {
count: ticksCount,
fontColor: "#8f8f8f"
},
}
},
@ -89,12 +90,12 @@
config.data.datasets[i].data = [0, 0, 0, 0, 0, 0, 0];
config.data.datasets[i].fill = true;
if(!data) {
if (!data) {
return;
}
let dateFormat = (value.range && range[value.range]) ? range[value.range] : 'd F Y';
for (let x = 0; x < data.length; x++) {
if(data[x].value > highest) {
highest = data[x].value;
@ -128,4 +129,4 @@
element.addEventListener('change', check);
}
});
})(window);
})(window);

View file

@ -3,13 +3,16 @@
window.ls.container.get("view").add({
selector: "data-forms-clone",
controller: function(element, document, view) {
controller: function(element, document, view, expression) {
element.removeAttribute('data-forms-clone');
view.render(element);
var template = element.innerHTML.toString();
var label = element.dataset["label"] || "Add";
var icon = element.dataset["icon"] || null;
var target = element.dataset["target"] || null;
var target = expression.parse(element.dataset["target"] || null);
var first = parseInt(element.dataset["first"] || 1);
var button = document.createElement("button");
var debug = element.dataset["debug"] || false;
button.type = "button";
button.innerText = " " + label + " ";
@ -42,6 +45,11 @@
view.render(clone);
if(debug) {
console.log('Debug: clone: ', clone);
console.log('Debug: target: ', target);
}
if (target) {
target.appendChild(clone);
} else {

View file

@ -0,0 +1,16 @@
(function(window) {
"use strict";
window.ls.container.get("view").add({
selector: "data-forms-ip-address",
controller: function(element) {
element.setAttribute("minlength", "7");
element.setAttribute("maxlength", "15");
element.setAttribute("size", "15");
element.setAttribute("autocomplete", "off");
element.setAttribute("title", "Please provide IPv4 or IPv6 address");
element.setAttribute("pattern", "((^\\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\\s*$)|(^\\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?\\s*$))");
}
});
})(window);

View file

@ -0,0 +1,15 @@
(function(window) {
"use strict";
window.ls.container.get("view").add({
selector: "data-forms-required",
controller: function(element, expression) {
const isRequired = expression.parse(element.getAttribute('data-forms-required')) === "true";
if (isRequired) {
element.setAttribute("required", true);
} else {
element.removeAttribute("disabled");
}
}
});
})(window);

View file

@ -0,0 +1,16 @@
(function(window) {
"use strict";
window.ls.container.get("view").add({
selector: "data-forms-selected",
controller: function(element, expression) {
const isSelected = expression.parse(element.getAttribute('data-forms-selected')) === element.getAttribute('value');
if (isSelected) {
element.setAttribute("selected", true);
} else {
element.removeAttribute("selected");
}
}
});
})(window);

View file

@ -21,8 +21,15 @@
let syncB = function() {
input.checked = (element.value === "true");
if (element.disabled) {
input.setAttribute("disabled", true);
} else {
input.removeAttribute("disabled");
}
};
input.addEventListener("input", syncA);
input.addEventListener("change", syncA);

View file

@ -154,12 +154,12 @@
case 'document':
document[element.key] = element.default || {'$id': '', '$collection': '', '$permissions': {}};
break;
default:
document[element.key] = null;
break;
}
if(element.array) {
document[element.key] = [];
}
@ -395,7 +395,9 @@
"failureParam" +
parsedFailure[i].charAt(0).toUpperCase() +
parsedFailure[i].slice(1),
{}
{
text: exception.message ?? undefined
}
)
);
}

View file

@ -78,7 +78,9 @@
.icon-ok-circled:before { content: '\e81a'; } /* '' */
.icon-warning:before { content: '\e81b'; } /* '' */
.icon-mail:before { content: '\e81c'; } /* '' */
.icon-email:before { content: '\e81c'; } /* '' */
.icon-link:before { content: '\e81d'; } /* '' */
.icon-url:before { content: '\e81d'; } /* '' */
.icon-key-inv:before { content: '\e81e'; } /* '' */
.icon-trash:before { content: '\e81f'; } /* '' */
.icon-download:before { content: '\e820'; } /* '' */
@ -134,6 +136,8 @@
.icon-string:before { content: '\e852'; } /* '' */
.icon-integer:before { content: '\e853'; } /* '' */
.icon-float:before { content: '\e854'; } /* '' */
.icon-double:before { content: '\e854'; } /* '' */
.icon-enum:before { content: '\e812'; } /* '' */
.icon-ip:before { content: '\e855'; } /* '' */
.icon-more:before { content: '\e856'; } /* '' */
.icon-key:before { content: '\e857'; } /* '' */

View file

@ -49,7 +49,7 @@ class PDO extends PDONative
return $this->pdo->setAttribute($attribute, $value);
}
public function prepare($statement, $driver_options = NULL)
public function prepare($statement, $driver_options = null)
{
return new PDOStatement($this, $this->pdo->prepare($statement, []));
}
@ -107,4 +107,4 @@ class PDO extends PDONative
return $this->pdo;
}
}
}

View file

@ -100,10 +100,10 @@ class PDOStatement extends PDOStatementNative
/**
* Fetch All
*
*
* @param int $fetch_style
* @param mixed $fetch_args
*
*
* @return array|false
*/
public function fetchAll(int $fetch_style = PDO::FETCH_BOTH, mixed ...$fetch_args)
@ -112,4 +112,4 @@ class PDOStatement extends PDOStatementNative
return $result;
}
}
}

View file

@ -4,7 +4,7 @@ namespace Appwrite\OpenSSL;
class OpenSSL
{
const CIPHER_AES_128_GCM = 'aes-128-gcm';
public const CIPHER_AES_128_GCM = 'aes-128-gcm';
/**
* @param $data

View file

@ -196,7 +196,7 @@ class Response extends SwooleResponse
->setModel(new BaseList('Documents List', self::MODEL_DOCUMENT_LIST, 'documents', self::MODEL_DOCUMENT))
->setModel(new BaseList('Users List', self::MODEL_USER_LIST, 'users', self::MODEL_USER))
->setModel(new BaseList('Sessions List', self::MODEL_SESSION_LIST, 'sessions', self::MODEL_SESSION))
->setModel(new BaseList('Logs List', self::MODEL_LOG_LIST, 'logs', self::MODEL_LOG, false))
->setModel(new BaseList('Logs List', self::MODEL_LOG_LIST, 'logs', self::MODEL_LOG))
->setModel(new BaseList('Files List', self::MODEL_FILE_LIST, 'files', self::MODEL_FILE))
->setModel(new BaseList('Buckets List', self::MODEL_BUCKET_LIST, 'buckets', self::MODEL_BUCKET))
->setModel(new BaseList('Teams List', self::MODEL_TEAM_LIST, 'teams', self::MODEL_TEAM))

View file

@ -16,35 +16,35 @@ class UsageCollection extends Model
'default' => '',
'example' => '30d',
])
->addRule('documents.count', [
->addRule('documentsCount', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for total number of documents.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('documents.create', [
->addRule('documentsCreate', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for documents created.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('documents.read', [
->addRule('documentsRead', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for documents read.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('documents.update', [
->addRule('documentsUpdate', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for documents updated.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('documents.delete', [
->addRule('documentsDelete', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for documents deleted.',
'default' => [],

View file

@ -16,70 +16,70 @@ class UsageDatabase extends Model
'default' => '',
'example' => '30d',
])
->addRule('documents.count', [
->addRule('documentsCount', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for total number of documents.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('collections.count', [
->addRule('collectionsCount', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for total number of collections.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('documents.create', [
->addRule('documentsCreate', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for documents created.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('documents.read', [
->addRule('documentsRead', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for documents read.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('documents.update', [
->addRule('documentsUpdate', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for documents updated.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('documents.delete', [
->addRule('documentsDelete', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for documents deleted.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('collections.create', [
->addRule('collectionsCreate', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for collections created.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('collections.read', [
->addRule('collectionsRead', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for collections read.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('collections.update', [
->addRule('collectionsUpdate', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for collections updated.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('collections.delete', [
->addRule('collectionsDelete', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for collections delete.',
'default' => [],

View file

@ -16,21 +16,21 @@ class UsageFunctions extends Model
'default' => '',
'example' => '30d',
])
->addRule('functions.executions', [
->addRule('functionsExecutions', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for function executions.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('functions.failures', [
->addRule('functionsFailures', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for function execution failures.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('functions.compute', [
->addRule('functionsCompute', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for function execution duration.',
'default' => [],

View file

@ -16,56 +16,56 @@ class UsageUsers extends Model
'default' => '',
'example' => '30d',
])
->addRule('users.count', [
->addRule('usersCount', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for total number of users.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('users.create', [
->addRule('usersCreate', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for users created.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('users.read', [
->addRule('usersRead', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for users read.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('users.update', [
->addRule('usersUpdate', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for users updated.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('users.delete', [
->addRule('usersDelete', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for users deleted.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('sessions.create', [
->addRule('sessionsCreate', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for sessions created.',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('sessions.provider.create', [
->addRule('sessionsProviderCreate', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for sessions created for a provider ( email, anonymous or oauth2 ).',
'default' => [],
'example' => new \stdClass,
'array' => true
])
->addRule('sessions.delete', [
->addRule('sessionsDelete', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for sessions deleted.',
'default' => [],

View file

@ -283,10 +283,10 @@ trait AccountBase
$this->assertEquals('', $response['body']['sessions'][0]['deviceBrand']);
$this->assertEquals('', $response['body']['sessions'][0]['deviceModel']);
$this->assertEquals($response['body']['sessions'][0]['ip'], filter_var($response['body']['sessions'][0]['ip'], FILTER_VALIDATE_IP));
$this->assertEquals('--', $response['body']['sessions'][0]['countryCode']);
$this->assertEquals('Unknown', $response['body']['sessions'][0]['countryName']);
$this->assertEquals(true, $response['body']['sessions'][0]['current']);
/**
@ -325,7 +325,8 @@ trait AccountBase
$this->assertIsArray($response['body']['logs']);
$this->assertNotEmpty($response['body']['logs']);
$this->assertCount(2, $response['body']['logs']);
$this->assertIsNumeric($response['body']['sum']);
$this->assertContains($response['body']['logs'][0]['event'], ['account.create', 'account.sessions.create']);
$this->assertEquals($response['body']['logs'][0]['ip'], filter_var($response['body']['logs'][0]['ip'], FILTER_VALIDATE_IP));
$this->assertIsNumeric($response['body']['logs'][0]['time']);
@ -344,7 +345,7 @@ trait AccountBase
$this->assertEquals('', $response['body']['logs'][0]['deviceBrand']);
$this->assertEquals('', $response['body']['logs'][0]['deviceModel']);
$this->assertEquals($response['body']['logs'][0]['ip'], filter_var($response['body']['logs'][0]['ip'], FILTER_VALIDATE_IP));
$this->assertEquals('--', $response['body']['logs'][0]['countryCode']);
$this->assertEquals('Unknown', $response['body']['logs'][0]['countryName']);
@ -366,10 +367,61 @@ trait AccountBase
$this->assertEquals('', $response['body']['logs'][1]['deviceBrand']);
$this->assertEquals('', $response['body']['logs'][1]['deviceModel']);
$this->assertEquals($response['body']['logs'][1]['ip'], filter_var($response['body']['logs'][1]['ip'], FILTER_VALIDATE_IP));
$this->assertEquals('--', $response['body']['logs'][1]['countryCode']);
$this->assertEquals('Unknown', $response['body']['logs'][1]['countryName']);
$responseLimit = $this->client->call(Client::METHOD_GET, '/account/logs', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session,
]), [
'limit' => 1
]);
$this->assertEquals($responseLimit['headers']['status-code'], 200);
$this->assertIsArray($responseLimit['body']['logs']);
$this->assertNotEmpty($responseLimit['body']['logs']);
$this->assertCount(1, $responseLimit['body']['logs']);
$this->assertIsNumeric($responseLimit['body']['sum']);
$this->assertEquals($response['body']['logs'][0], $responseLimit['body']['logs'][0]);
$responseOffset = $this->client->call(Client::METHOD_GET, '/account/logs', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session,
]), [
'offset' => 1
]);
$this->assertEquals($responseOffset['headers']['status-code'], 200);
$this->assertIsArray($responseOffset['body']['logs']);
$this->assertNotEmpty($responseOffset['body']['logs']);
$this->assertCount(1, $responseOffset['body']['logs']);
$this->assertIsNumeric($responseOffset['body']['sum']);
$this->assertEquals($response['body']['logs'][1], $responseOffset['body']['logs'][0]);
$responseLimitOffset = $this->client->call(Client::METHOD_GET, '/account/logs', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session,
]), [
'limit' => 1,
'offset' => 1
]);
$this->assertEquals($responseLimitOffset['headers']['status-code'], 200);
$this->assertIsArray($responseLimitOffset['body']['logs']);
$this->assertNotEmpty($responseLimitOffset['body']['logs']);
$this->assertCount(1, $responseLimitOffset['body']['logs']);
$this->assertIsNumeric($responseLimitOffset['body']['sum']);
$this->assertEquals($response['body']['logs'][1], $responseLimitOffset['body']['logs'][0]);
/**
* Test for FAILURE
*/

View file

@ -595,11 +595,27 @@ trait DatabaseBase
'attributes' => ['title'],
]);
$this->assertEquals($titleIndex['headers']['status-code'], 201);
$this->assertEquals($titleIndex['body']['key'], 'titleIndex');
$this->assertEquals($titleIndex['body']['type'], 'fulltext');
$this->assertEquals(201, $titleIndex['headers']['status-code']);
$this->assertEquals('titleIndex', $titleIndex['body']['key']);
$this->assertEquals('fulltext', $titleIndex['body']['type']);
$this->assertCount(1, $titleIndex['body']['attributes']);
$this->assertEquals($titleIndex['body']['attributes'][0], 'title');
$this->assertEquals('title', $titleIndex['body']['attributes'][0]);
$releaseYearIndex = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['moviesId'] . '/indexes', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'indexId' => 'releaseYear',
'type' => 'key',
'attributes' => ['releaseYear'],
]);
$this->assertEquals(201, $releaseYearIndex['headers']['status-code']);
$this->assertEquals('releaseYear', $releaseYearIndex['body']['key']);
$this->assertEquals('key', $releaseYearIndex['body']['type']);
$this->assertCount(1, $releaseYearIndex['body']['attributes']);
$this->assertEquals('releaseYear', $releaseYearIndex['body']['attributes'][0]);
// wait for database worker to create index
sleep(2);
@ -611,8 +627,11 @@ trait DatabaseBase
]), []);
$this->assertIsArray($movies['body']['indexes']);
$this->assertCount(1, $movies['body']['indexes']);
$this->assertEquals($movies['body']['indexes'][0]['key'], $titleIndex['body']['key']);
$this->assertCount(2, $movies['body']['indexes']);
$this->assertEquals($titleIndex['body']['key'], $movies['body']['indexes'][0]['key']);
$this->assertEquals($releaseYearIndex['body']['key'], $movies['body']['indexes'][1]['key']);
$this->assertEquals('available', $movies['body']['indexes'][0]['status']);
$this->assertEquals('available', $movies['body']['indexes'][1]['status']);
return $data;
}
@ -1024,93 +1043,77 @@ trait DatabaseBase
/**
* @depends testCreateDocument
*/
// public function testDocumentsListSuccessSearch(array $data):array
// {
// $documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['moviesId'] . '/documents', array_merge([
// 'content-type' => 'application/json',
// 'x-appwrite-project' => $this->getProject()['$id'],
// ], $this->getHeaders()), [
// 'queries' => ['title.search("Captain America")'],
// ]);
public function testDocumentsListQueries(array $data):array
{
$documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['moviesId'] . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['title.search("Captain America")'],
]);
// var_dump($documents);
$this->assertEquals($documents['headers']['status-code'], 200);
$this->assertEquals(1944, $documents['body']['documents'][0]['releaseYear']);
$this->assertCount(1, $documents['body']['documents']);
// $this->assertEquals($documents['headers']['status-code'], 200);
// $this->assertEquals(1944, $documents['body']['documents'][0]['releaseYear']);
// $this->assertCount(1, $documents['body']['documents']);
$documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['moviesId'] . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['title.search("Homecoming")'],
]);
// $documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['moviesId'] . '/documents', array_merge([
// 'content-type' => 'application/json',
// 'x-appwrite-project' => $this->getProject()['$id'],
// ], $this->getHeaders()), [
// 'queries' => ['title.search("Homecoming")'],
// ]);
$this->assertEquals($documents['headers']['status-code'], 200);
$this->assertEquals(2017, $documents['body']['documents'][0]['releaseYear']);
$this->assertCount(1, $documents['body']['documents']);
// $this->assertEquals($documents['headers']['status-code'], 200);
// $this->assertEquals(2017, $documents['body']['documents'][0]['releaseYear']);
// $this->assertCount(1, $documents['body']['documents']);
$documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['moviesId'] . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['title.search("spider")'],
]);
// $documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['moviesId'] . '/documents', array_merge([
// 'content-type' => 'application/json',
// 'x-appwrite-project' => $this->getProject()['$id'],
// ], $this->getHeaders()), [
// 'queries' => ['title.search("spider")'],
// ]);
$this->assertEquals($documents['headers']['status-code'], 200);
$this->assertEquals(2019, $documents['body']['documents'][0]['releaseYear']);
$this->assertEquals(2017, $documents['body']['documents'][1]['releaseYear']);
$this->assertCount(2, $documents['body']['documents']);
// $this->assertEquals($documents['headers']['status-code'], 200);
// $this->assertEquals(2019, $documents['body']['documents'][0]['releaseYear']);
// $this->assertEquals(2017, $documents['body']['documents'][1]['releaseYear']);
// $this->assertCount(2, $documents['body']['documents']);
$documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['moviesId'] . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['releaseYear.equal(1944)'],
]);
// return [];
// }
// TODO@kodumbeats test for empty searches and misformatted queries
$this->assertCount(1, $documents['body']['documents']);
$this->assertEquals('Captain America', $documents['body']['documents'][0]['title']);
/**
* @depends testCreateDocument
*/
// public function testListDocumentsFilters(array $data):array
// {
// $documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['moviesId'] . '/documents', array_merge([
// 'content-type' => 'application/json',
// 'x-appwrite-project' => $this->getProject()['$id'],
// ], $this->getHeaders()), [
// 'filters' => [
// 'actors.firstName=Tom'
// ],
// ]);
$documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['moviesId'] . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['releaseYear.notEqual(1944)'],
]);
// $this->assertCount(2, $documents['body']['documents']);
// $this->assertEquals('Spider-Man: Far From Home', $documents['body']['documents'][0]['name']);
// $this->assertEquals('Spider-Man: Homecoming', $documents['body']['documents'][1]['name']);
$this->assertCount(2, $documents['body']['documents']);
$this->assertEquals('Spider-Man: Far From Home', $documents['body']['documents'][0]['title']);
$this->assertEquals('Spider-Man: Homecoming', $documents['body']['documents'][1]['title']);
// $documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['moviesId'] . '/documents', array_merge([
// 'content-type' => 'application/json',
// 'x-appwrite-project' => $this->getProject()['$id'],
// ], $this->getHeaders()), [
// 'filters' => [
// 'releaseYear=1944'
// ],
// ]);
/**
* Test for Failure
*/
$documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['moviesId'] . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['actors.equal("Tom Holland")'],
]);
$this->assertEquals(400, $documents['headers']['status-code']);
$this->assertEquals('Index not found: actors', $documents['body']['message']);
// $this->assertCount(1, $documents['body']['documents']);
// $this->assertEquals('Captain America', $documents['body']['documents'][0]['name']);
// $documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['moviesId'] . '/documents', array_merge([
// 'content-type' => 'application/json',
// 'x-appwrite-project' => $this->getProject()['$id'],
// ], $this->getHeaders()), [
// 'filters' => [
// 'releaseYear!=1944'
// ],
// ]);
// $this->assertCount(2, $documents['body']['documents']);
// $this->assertEquals('Spider-Man: Far From Home', $documents['body']['documents'][0]['name']);
// $this->assertEquals('Spider-Man: Homecoming', $documents['body']['documents'][1]['name']);
// return [];
// }
return [];
}
/**
* @depends testCreateDocument

View file

@ -12,7 +12,7 @@ class DatabaseConsoleClientTest extends Scope
use ProjectCustom;
use SideConsole;
public function testCreateCollection():array
public function testCreateCollection(): array
{
/**
* Test for SUCCESS
@ -33,7 +33,7 @@ class DatabaseConsoleClientTest extends Scope
return ['moviesId' => $movies['body']['$id']];
}
public function testGetDatabaseUsage()
{
/**
@ -48,7 +48,7 @@ class DatabaseConsoleClientTest extends Scope
]);
$this->assertEquals($response['headers']['status-code'], 400);
/**
* Test for SUCCESS
*/
@ -63,16 +63,16 @@ class DatabaseConsoleClientTest extends Scope
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertEquals(count($response['body']), 11);
$this->assertEquals($response['body']['range'], '24h');
$this->assertIsArray($response['body']['documents.count']);
$this->assertIsArray($response['body']['collections.count']);
$this->assertIsArray($response['body']['documents.create']);
$this->assertIsArray($response['body']['documents.read']);
$this->assertIsArray($response['body']['documents.update']);
$this->assertIsArray($response['body']['documents.delete']);
$this->assertIsArray($response['body']['collections.create']);
$this->assertIsArray($response['body']['collections.read']);
$this->assertIsArray($response['body']['collections.update']);
$this->assertIsArray($response['body']['collections.delete']);
$this->assertIsArray($response['body']['documentsCount']);
$this->assertIsArray($response['body']['collectionsCount']);
$this->assertIsArray($response['body']['documentsCreate']);
$this->assertIsArray($response['body']['documentsRead']);
$this->assertIsArray($response['body']['documentsUpdate']);
$this->assertIsArray($response['body']['documentsDelete']);
$this->assertIsArray($response['body']['collectionsCreate']);
$this->assertIsArray($response['body']['collectionsRead']);
$this->assertIsArray($response['body']['collectionsUpdate']);
$this->assertIsArray($response['body']['collectionsDelete']);
}
@ -85,7 +85,7 @@ class DatabaseConsoleClientTest extends Scope
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/database/'.$data['moviesId'].'/usage', array_merge([
$response = $this->client->call(Client::METHOD_GET, '/database/' . $data['moviesId'] . '/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
@ -106,7 +106,7 @@ class DatabaseConsoleClientTest extends Scope
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/database/'.$data['moviesId'].'/usage', array_merge([
$response = $this->client->call(Client::METHOD_GET, '/database/' . $data['moviesId'] . '/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
@ -116,10 +116,64 @@ class DatabaseConsoleClientTest extends Scope
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertEquals(count($response['body']), 6);
$this->assertEquals($response['body']['range'], '24h');
$this->assertIsArray($response['body']['documents.count']);
$this->assertIsArray($response['body']['documents.create']);
$this->assertIsArray($response['body']['documents.read']);
$this->assertIsArray($response['body']['documents.update']);
$this->assertIsArray($response['body']['documents.delete']);
$this->assertIsArray($response['body']['documentsCount']);
$this->assertIsArray($response['body']['documentsCreate']);
$this->assertIsArray($response['body']['documentsRead']);
$this->assertIsArray($response['body']['documentsUpdate']);
$this->assertIsArray($response['body']['documentsDelete']);
}
}
/**
* @depends testCreateCollection
*/
public function testGetCollectionLogs(array $data)
{
/**
* Test for SUCCESS
*/
$logs = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['moviesId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['sum']);
$logs = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['moviesId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'limit' => 1
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['sum']);
$logs = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['moviesId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'offset' => 1
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['sum']);
$logs = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['moviesId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'offset' => 1,
'limit' => 1
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['sum']);
}
}

View file

@ -193,6 +193,22 @@ class DatabaseCustomServerTest extends Scope
// Wait for database worker to finish creating attributes
sleep(2);
// Creating document to ensure cache is purged on schema change
$document = $this->client->call(Client::METHOD_POST, '/database/collections/' . $actors['body']['$id'] . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'documentId' => 'unique()',
'data' => [
'firstName' => 'lorem',
'lastName' => 'ipsum',
'unneeded' => 'dolor'
],
'read' => ['role:all'],
'write' => ['role:all'],
]);
$index = $this->client->call(Client::METHOD_POST, '/database/collections/' . $actors['body']['$id'] . '/indexes', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@ -236,6 +252,15 @@ class DatabaseCustomServerTest extends Scope
sleep(2);
// Check document to ensure cache is purged on schema change
$document = $this->client->call(Client::METHOD_GET, '/database/collections/' . $actors['body']['$id'] . '/documents/' . $document['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertNotContains($unneededId, $document['body']);
$collection = $this->client->call(Client::METHOD_GET, '/database/collections/' . $actors['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],

View file

@ -12,7 +12,7 @@ class FunctionsConsoleClientTest extends Scope
use ProjectCustom;
use SideConsole;
public function testCreateFunction():array
public function testCreateFunction(): array
{
$function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([
'content-type' => 'application/json',
@ -20,7 +20,7 @@ class FunctionsConsoleClientTest extends Scope
], $this->getHeaders()), [
'functionId' => 'unique()',
'name' => 'Test',
'execute' => ['user:'.$this->getUser()['$id']],
'execute' => ['user:' . $this->getUser()['$id']],
'runtime' => 'php-8.0',
'vars' => [
'funcKey1' => 'funcValue1',
@ -41,7 +41,7 @@ class FunctionsConsoleClientTest extends Scope
'functionId' => $function['body']['$id']
];
}
/**
* @depends testCreateFunction
*/
@ -51,7 +51,7 @@ class FunctionsConsoleClientTest extends Scope
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/functions/'.$data['functionId'].'/usage', array_merge([
$response = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
@ -73,7 +73,7 @@ class FunctionsConsoleClientTest extends Scope
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/functions/'.$data['functionId'].'/usage', array_merge([
$response = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
@ -83,9 +83,8 @@ class FunctionsConsoleClientTest extends Scope
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertEquals(count($response['body']), 4);
$this->assertEquals($response['body']['range'], '24h');
$this->assertIsArray($response['body']['functions.executions']);
$this->assertIsArray($response['body']['functions.failures']);
$this->assertIsArray($response['body']['functions.compute']);
$this->assertIsArray($response['body']['functionsExecutions']);
$this->assertIsArray($response['body']['functionsFailures']);
$this->assertIsArray($response['body']['functionsCompute']);
}
}
}

View file

@ -14,7 +14,7 @@ class HealthCustomServerTest extends Scope
use ProjectCustom;
use SideServer;
public function testHTTPSuccess():array
public function testHTTPSuccess(): array
{
/**
* Test for SUCCESS
@ -30,11 +30,11 @@ class HealthCustomServerTest extends Scope
/**
* Test for FAILURE
*/
return [];
}
public function testDBSuccess():array
public function testDBSuccess(): array
{
/**
* Test for SUCCESS
@ -50,11 +50,11 @@ class HealthCustomServerTest extends Scope
/**
* Test for FAILURE
*/
return [];
}
public function testCacheSuccess():array
public function testCacheSuccess(): array
{
/**
* Test for SUCCESS
@ -70,11 +70,11 @@ class HealthCustomServerTest extends Scope
/**
* Test for FAILURE
*/
return [];
}
public function testTimeSuccess():array
public function testTimeSuccess(): array
{
/**
* Test for SUCCESS
@ -94,11 +94,11 @@ class HealthCustomServerTest extends Scope
/**
* Test for FAILURE
*/
return [];
}
public function testWebhooksSuccess():array
public function testWebhooksSuccess(): array
{
/**
* Test for SUCCESS
@ -115,11 +115,11 @@ class HealthCustomServerTest extends Scope
/**
* Test for FAILURE
*/
return [];
}
public function testLogsSuccess():array
public function testLogsSuccess(): array
{
/**
* Test for SUCCESS
@ -136,11 +136,11 @@ class HealthCustomServerTest extends Scope
/**
* Test for FAILURE
*/
return [];
}
public function testUsageSuccess():array
public function testUsageSuccess(): array
{
/**
* Test for SUCCESS
@ -157,11 +157,11 @@ class HealthCustomServerTest extends Scope
/**
* Test for FAILURE
*/
return [];
}
public function testCertificatesSuccess():array
public function testCertificatesSuccess(): array
{
/**
* Test for SUCCESS
@ -178,11 +178,11 @@ class HealthCustomServerTest extends Scope
/**
* Test for FAILURE
*/
return [];
}
public function testStorageLocalSuccess():array
public function testStorageLocalSuccess(): array
{
/**
* Test for SUCCESS
@ -198,11 +198,11 @@ class HealthCustomServerTest extends Scope
/**
* Test for FAILURE
*/
return [];
}
public function testStorageAntiVirusSuccess():array
public function testStorageAntiVirusSuccess(): array
{
/**
* Test for SUCCESS
@ -220,7 +220,7 @@ class HealthCustomServerTest extends Scope
/**
* Test for FAILURE
*/
return [];
}
}
}

View file

@ -60,7 +60,7 @@ trait RealtimeBase
$this->expectException(ConnectionException::class); // Check if server disconnnected client
$client->close();
$client = new WebSocketClient('ws://appwrite-traefik/v1/realtime', [
$client = new WebSocketClient('ws://appwrite-traefik/v1/realtime?channels[]=files"', [
'headers' => [
'Origin' => 'appwrite.test'
]

View file

@ -26,7 +26,7 @@ class StorageConsoleClientTest extends Scope
]);
$this->assertEquals($response['headers']['status-code'], 400);
/**
* Test for SUCCESS
*/
@ -102,4 +102,4 @@ class StorageConsoleClientTest extends Scope
$this->assertIsArray($response['body']['filesDelete']);
$this->assertIsArray($response['body']['filesStorage']);
}
}
}

View file

@ -73,8 +73,8 @@ trait TeamsBaseClient
$this->assertEquals('Invitation to '.$teamName.' Team at '.$this->getProject()['name'], $lastEmail['subject']);
$secret = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256);
$membershipUid = substr($lastEmail['text'], strpos($lastEmail['text'], '?membershipId=', 0) + 14, 13);
$userUid = substr($lastEmail['text'], strpos($lastEmail['text'], '&userId=', 0) + 8, 13);
$membershipUid = substr($lastEmail['text'], strpos($lastEmail['text'], '?membershipId=', 0) + 14, 20);
$userUid = substr($lastEmail['text'], strpos($lastEmail['text'], '&userId=', 0) + 8, 20);
/**
* Test for FAILURE

View file

@ -204,14 +204,6 @@ trait UsersBase
$this->assertEquals($sessions['headers']['status-code'], 200);
$this->assertIsArray($sessions['body']);
$logs = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']);
$users = $this->client->call(Client::METHOD_GET, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@ -428,6 +420,61 @@ trait UsersBase
return $data;
}
/**
* @depends testGetUser
*/
public function testGetLogs(array $data): void
{
/**
* Test for SUCCESS
*/
$logs = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['sum']);
$logs = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'limit' => 1
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['sum']);
$logs = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'offset' => 1
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['sum']);
$logs = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'offset' => 1,
'limit' => 1
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['sum']);
}
/**
* @depends testGetUser
*/

View file

@ -37,7 +37,7 @@ class UsersConsoleClientTest extends Scope
]);
$this->assertEquals($response['headers']['status-code'], 400);
/**
* Test for SUCCESS
*/
@ -52,15 +52,15 @@ class UsersConsoleClientTest extends Scope
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertEquals(count($response['body']), 9);
$this->assertEquals($response['body']['range'], '24h');
$this->assertIsArray($response['body']['users.count']);
$this->assertIsArray($response['body']['users.create']);
$this->assertIsArray($response['body']['users.read']);
$this->assertIsArray($response['body']['users.update']);
$this->assertIsArray($response['body']['users.delete']);
$this->assertIsArray($response['body']['sessions.create']);
$this->assertIsArray($response['body']['sessions.provider.create']);
$this->assertIsArray($response['body']['sessions.delete']);
$this->assertIsArray($response['body']['usersCount']);
$this->assertIsArray($response['body']['usersCreate']);
$this->assertIsArray($response['body']['usersRead']);
$this->assertIsArray($response['body']['usersUpdate']);
$this->assertIsArray($response['body']['usersDelete']);
$this->assertIsArray($response['body']['sessionsCreate']);
$this->assertIsArray($response['body']['sessionsProviderCreate']);
$this->assertIsArray($response['body']['sessionsDelete']);
$response = $this->client->call(Client::METHOD_GET, '/users/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
@ -71,13 +71,13 @@ class UsersConsoleClientTest extends Scope
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertEquals(count($response['body']), 9);
$this->assertEquals($response['body']['range'], '24h');
$this->assertIsArray($response['body']['users.count']);
$this->assertIsArray($response['body']['users.create']);
$this->assertIsArray($response['body']['users.read']);
$this->assertIsArray($response['body']['users.update']);
$this->assertIsArray($response['body']['users.delete']);
$this->assertIsArray($response['body']['sessions.create']);
$this->assertIsArray($response['body']['sessions.provider.create']);
$this->assertIsArray($response['body']['sessions.delete']);
$this->assertIsArray($response['body']['usersCount']);
$this->assertIsArray($response['body']['usersCreate']);
$this->assertIsArray($response['body']['usersRead']);
$this->assertIsArray($response['body']['usersUpdate']);
$this->assertIsArray($response['body']['usersDelete']);
$this->assertIsArray($response['body']['sessionsCreate']);
$this->assertIsArray($response['body']['sessionsProviderCreate']);
$this->assertIsArray($response['body']['sessionsDelete']);
}
}
}

View file

@ -662,8 +662,8 @@ trait WebhooksBase
$lastEmail = $this->getLastEmail();
$secret = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256);
$membershipUid = substr($lastEmail['text'], strpos($lastEmail['text'], '?membershipId=', 0) + 14, 13);
$userUid = substr($lastEmail['text'], strpos($lastEmail['text'], '&userId=', 0) + 8, 13);
$membershipUid = substr($lastEmail['text'], strpos($lastEmail['text'], '?membershipId=', 0) + 14, 20);
$userUid = substr($lastEmail['text'], strpos($lastEmail['text'], '&userId=', 0) + 8, 20);
$webhook = $this->getLastRequest();