Merge branch '1.8.x' of github.com:appwrite/appwrite into feat-mongodb

# Conflicts:
#	composer.json
#	composer.lock
This commit is contained in:
shimon
2025-08-06 15:48:19 +03:00
41 changed files with 7304 additions and 672 deletions
+9
View File
@@ -51,6 +51,15 @@ return [
'default' => null,
'array' => false,
],
[
'$id' => ID::custom('type'),
'type' => Database::VAR_STRING,
'size' => 128,
'required' => true,
'signed' => true,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
+28 -6
View File
@@ -25,6 +25,7 @@ return [
'gitRepoName' => 'sdk-for-web',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/web/CHANGELOG.md'),
'demos' => [
[
'icon' => 'react.svg',
@@ -73,6 +74,7 @@ return [
'gitRepoName' => 'sdk-for-flutter',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/flutter/CHANGELOG.md'),
],
[
'key' => 'apple',
@@ -91,6 +93,7 @@ return [
'gitRepoName' => 'sdk-for-apple',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/apple/CHANGELOG.md'),
],
[
'key' => 'objective-c',
@@ -108,6 +111,7 @@ return [
'gitRepoName' => 'sdk-for-objective-c',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/objective-c/CHANGELOG.md'),
],
[
'key' => 'android',
@@ -130,6 +134,7 @@ return [
'Kotlin' => 'kotlin',
'Java' => 'java',
],
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/android/CHANGELOG.md'),
],
[
'key' => 'react-native',
@@ -148,6 +153,7 @@ return [
'gitRepoName' => 'sdk-for-react-native',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/react-native/CHANGELOG.md'),
],
[
'key' => 'graphql',
@@ -167,6 +173,7 @@ return [
'gitUserName' => '',
'gitBranch' => '',
'isSDK' => false,
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/graphql/CHANGELOG.md'),
],
[
'key' => 'rest',
@@ -186,6 +193,7 @@ return [
'gitUserName' => '',
'gitBranch' => '',
'isSDK' => false,
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/rest/CHANGELOG.md'),
],
],
],
@@ -199,8 +207,8 @@ return [
[
'key' => 'web',
'name' => 'Console',
'version' => '1.9.0',
'url' => 'https://github.com/appwrite/sdk-for-console',
'version' => '0.1.0',
'url' => '',
'package' => '',
'enabled' => true,
'beta' => false,
@@ -209,10 +217,11 @@ return [
'family' => APP_PLATFORM_CONSOLE,
'prism' => 'javascript',
'source' => \realpath(__DIR__ . '/../sdks/console-web'),
'gitUrl' => 'git@github.com:appwrite/sdk-for-console.git',
'gitUrl' => '',
'gitBranch' => 'dev',
'gitRepoName' => 'sdk-for-console',
'gitUserName' => 'appwrite',
'gitRepoName' => '',
'gitUserName' => '',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/console/CHANGELOG.md'),
],
[
'key' => 'cli',
@@ -232,6 +241,7 @@ return [
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'repoBranch' => 'master',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/cli/CHANGELOG.md'),
'exclude' => [
'services' => [
['name' => 'assistant'],
@@ -265,6 +275,7 @@ return [
'gitRepoName' => 'sdk-for-node',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/nodejs/CHANGELOG.md'),
],
[
'key' => 'deno',
@@ -283,6 +294,7 @@ return [
'gitRepoName' => 'sdk-for-deno',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/deno/CHANGELOG.md'),
],
[
'key' => 'php',
@@ -301,6 +313,7 @@ return [
'gitRepoName' => 'sdk-for-php',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/php/CHANGELOG.md'),
],
[
'key' => 'python',
@@ -319,6 +332,7 @@ return [
'gitRepoName' => 'sdk-for-python',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/python/CHANGELOG.md'),
],
[
'key' => 'ruby',
@@ -337,6 +351,7 @@ return [
'gitRepoName' => 'sdk-for-ruby',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/ruby/CHANGELOG.md'),
],
[
'key' => 'go',
@@ -355,11 +370,12 @@ return [
'gitRepoName' => 'sdk-for-go',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/go/CHANGELOG.md'),
],
[
'key' => 'dotnet',
'name' => '.NET',
'version' => '0.14.1',
'version' => '0.15.0',
'url' => 'https://github.com/appwrite/sdk-for-dotnet',
'package' => 'https://www.nuget.org/packages/Appwrite',
'enabled' => true,
@@ -373,6 +389,7 @@ return [
'gitRepoName' => 'sdk-for-dotnet',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/dotnet/CHANGELOG.md'),
],
[
'key' => 'dart',
@@ -391,6 +408,7 @@ return [
'gitRepoName' => 'sdk-for-dart',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/dart/CHANGELOG.md'),
],
[
'key' => 'kotlin',
@@ -413,6 +431,7 @@ return [
'Kotlin' => 'kotlin',
'Java' => 'java',
],
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/kotlin/CHANGELOG.md'),
],
[
'key' => 'swift',
@@ -431,6 +450,7 @@ return [
'gitRepoName' => 'sdk-for-swift',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/swift/CHANGELOG.md'),
],
[
'key' => 'graphql',
@@ -450,6 +470,7 @@ return [
'gitUserName' => '',
'gitBranch' => '',
'isSDK' => false,
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/graphql/CHANGELOG.md'),
],
[
'key' => 'rest',
@@ -469,6 +490,7 @@ return [
'gitUserName' => '',
'gitBranch' => '',
'isSDK' => false,
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/rest/CHANGELOG.md'),
],
],
],
+439 -50
View File
@@ -5361,7 +5361,7 @@
"scheduledAt": {
"type": "string",
"description": "Scheduled execution time in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. DateTime value must be in future with precision in minutes.",
"x-example": null
"x-example": "<SCHEDULED_AT>"
}
}
}
@@ -8033,7 +8033,8 @@
"any": {
"description": "Any",
"type": "object",
"additionalProperties": true
"additionalProperties": true,
"example": []
},
"error": {
"description": "Error",
@@ -8065,7 +8066,13 @@
"code",
"type",
"version"
]
],
"example": {
"message": "Not found",
"code": "404",
"type": "not_found",
"version": "1.0"
}
},
"documentList": {
"description": "Documents List",
@@ -8089,7 +8096,11 @@
"required": [
"total",
"documents"
]
],
"example": {
"total": 5,
"documents": ""
}
},
"sessionList": {
"description": "Sessions List",
@@ -8113,7 +8124,11 @@
"required": [
"total",
"sessions"
]
],
"example": {
"total": 5,
"sessions": ""
}
},
"identityList": {
"description": "Identities List",
@@ -8137,7 +8152,11 @@
"required": [
"total",
"identities"
]
],
"example": {
"total": 5,
"identities": ""
}
},
"logList": {
"description": "Logs List",
@@ -8161,7 +8180,11 @@
"required": [
"total",
"logs"
]
],
"example": {
"total": 5,
"logs": ""
}
},
"fileList": {
"description": "Files List",
@@ -8185,7 +8208,11 @@
"required": [
"total",
"files"
]
],
"example": {
"total": 5,
"files": ""
}
},
"teamList": {
"description": "Teams List",
@@ -8209,7 +8236,11 @@
"required": [
"total",
"teams"
]
],
"example": {
"total": 5,
"teams": ""
}
},
"membershipList": {
"description": "Memberships List",
@@ -8233,7 +8264,11 @@
"required": [
"total",
"memberships"
]
],
"example": {
"total": 5,
"memberships": ""
}
},
"executionList": {
"description": "Executions List",
@@ -8257,7 +8292,11 @@
"required": [
"total",
"executions"
]
],
"example": {
"total": 5,
"executions": ""
}
},
"countryList": {
"description": "Countries List",
@@ -8281,7 +8320,11 @@
"required": [
"total",
"countries"
]
],
"example": {
"total": 5,
"countries": ""
}
},
"continentList": {
"description": "Continents List",
@@ -8305,7 +8348,11 @@
"required": [
"total",
"continents"
]
],
"example": {
"total": 5,
"continents": ""
}
},
"languageList": {
"description": "Languages List",
@@ -8329,7 +8376,11 @@
"required": [
"total",
"languages"
]
],
"example": {
"total": 5,
"languages": ""
}
},
"currencyList": {
"description": "Currencies List",
@@ -8353,7 +8404,11 @@
"required": [
"total",
"currencies"
]
],
"example": {
"total": 5,
"currencies": ""
}
},
"phoneList": {
"description": "Phones List",
@@ -8377,7 +8432,11 @@
"required": [
"total",
"phones"
]
],
"example": {
"total": 5,
"phones": ""
}
},
"localeCodeList": {
"description": "Locale codes list",
@@ -8401,7 +8460,11 @@
"required": [
"total",
"localeCodes"
]
],
"example": {
"total": 5,
"localeCodes": ""
}
},
"document": {
"description": "Document",
@@ -8458,7 +8521,23 @@
"$createdAt",
"$updatedAt",
"$permissions"
]
],
"example": {
"$id": "5e5ea5c16897e",
"$sequence": 1,
"$collectionId": "5e5ea5c15117e",
"$databaseId": "5e5ea5c15117e",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"$updatedAt": "2020-10-15T06:38:00.000+00:00",
"$permissions": [
"read(\"any\")"
],
"username": "john.doe",
"email": "john.doe@example.com",
"fullName": "John Doe",
"age": 30,
"isAdmin": false
}
},
"log": {
"description": "Log",
@@ -8592,7 +8671,30 @@
"deviceModel",
"countryCode",
"countryName"
]
],
"example": {
"event": "account.sessions.create",
"userId": "610fc2f985ee0",
"userEmail": "john@appwrite.io",
"userName": "John Doe",
"mode": "admin",
"ip": "127.0.0.1",
"time": "2020-10-15T06:38:00.000+00:00",
"osCode": "Mac",
"osName": "Mac",
"osVersion": "Mac",
"clientType": "browser",
"clientCode": "CM",
"clientName": "Chrome Mobile iOS",
"clientVersion": "84.0",
"clientEngine": "WebKit",
"clientEngineVersion": "605.1.15",
"deviceName": "smartphone",
"deviceBrand": "Google",
"deviceModel": "Nexus 5",
"countryCode": "US",
"countryName": "United States"
}
},
"user": {
"description": "User",
@@ -8753,7 +8855,33 @@
"prefs",
"targets",
"accessedAt"
]
],
"example": {
"$id": "5e5ea5c16897e",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"$updatedAt": "2020-10-15T06:38:00.000+00:00",
"name": "John Doe",
"password": "$argon2id$v=19$m=2048,t=4,p=3$aUZjLnliVWRINmFNTWMudg$5S+x+7uA31xFnrHFT47yFwcJeaP0w92L\/4LdgrVRXxE",
"hash": "argon2",
"hashOptions": {},
"registration": "2020-10-15T06:38:00.000+00:00",
"status": true,
"labels": [
"vip"
],
"passwordUpdate": "2020-10-15T06:38:00.000+00:00",
"email": "john@appwrite.io",
"phone": "+4930901820",
"emailVerification": true,
"phoneVerification": true,
"mfa": true,
"prefs": {
"theme": "pink",
"timezone": "UTC"
},
"targets": [],
"accessedAt": "2020-10-15T06:38:00.000+00:00"
}
},
"algoMd5": {
"description": "AlgoMD5",
@@ -8767,7 +8895,10 @@
},
"required": [
"type"
]
],
"example": {
"type": "md5"
}
},
"algoSha": {
"description": "AlgoSHA",
@@ -8781,7 +8912,10 @@
},
"required": [
"type"
]
],
"example": {
"type": "sha"
}
},
"algoPhpass": {
"description": "AlgoPHPass",
@@ -8795,7 +8929,10 @@
},
"required": [
"type"
]
],
"example": {
"type": "phpass"
}
},
"algoBcrypt": {
"description": "AlgoBcrypt",
@@ -8809,7 +8946,10 @@
},
"required": [
"type"
]
],
"example": {
"type": "bcrypt"
}
},
"algoScrypt": {
"description": "AlgoScrypt",
@@ -8851,7 +8991,14 @@
"costMemory",
"costParallel",
"length"
]
],
"example": {
"type": "scrypt",
"costCpu": 8,
"costMemory": 14,
"costParallel": 1,
"length": 64
}
},
"algoScryptModified": {
"description": "AlgoScryptModified",
@@ -8883,7 +9030,13 @@
"salt",
"saltSeparator",
"signerKey"
]
],
"example": {
"type": "scryptMod",
"salt": "UxLMreBr6tYyjQ==",
"saltSeparator": "Bw==",
"signerKey": "XyEKE9RcTDeLEsL\/RjwPDBv\/RqDl8fb3gpYEOQaPihbxf1ZAtSOHCjuAAa7Q3oHpCYhXSN9tizHgVOwn6krflQ=="
}
},
"algoArgon2": {
"description": "AlgoArgon2",
@@ -8918,12 +9071,23 @@
"memoryCost",
"timeCost",
"threads"
]
],
"example": {
"type": "argon2",
"memoryCost": 65536,
"timeCost": 4,
"threads": 3
}
},
"preferences": {
"description": "Preferences",
"type": "object",
"additionalProperties": true
"additionalProperties": true,
"example": {
"language": "en",
"timezone": "UTC",
"darkTheme": true
}
},
"session": {
"description": "Session",
@@ -9110,7 +9274,40 @@
"factors",
"secret",
"mfaUpdatedAt"
]
],
"example": {
"$id": "5e5ea5c16897e",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"$updatedAt": "2020-10-15T06:38:00.000+00:00",
"userId": "5e5bb8c16897e",
"expire": "2020-10-15T06:38:00.000+00:00",
"provider": "email",
"providerUid": "user@example.com",
"providerAccessToken": "MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3",
"providerAccessTokenExpiry": "2020-10-15T06:38:00.000+00:00",
"providerRefreshToken": "MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3",
"ip": "127.0.0.1",
"osCode": "Mac",
"osName": "Mac",
"osVersion": "Mac",
"clientType": "browser",
"clientCode": "CM",
"clientName": "Chrome Mobile iOS",
"clientVersion": "84.0",
"clientEngine": "WebKit",
"clientEngineVersion": "605.1.15",
"deviceName": "smartphone",
"deviceBrand": "Google",
"deviceModel": "Nexus 5",
"countryCode": "US",
"countryName": "United States",
"current": true,
"factors": [
"email"
],
"secret": "5e5bb8c16897e",
"mfaUpdatedAt": "2020-10-15T06:38:00.000+00:00"
}
},
"identity": {
"description": "Identity",
@@ -9178,7 +9375,19 @@
"providerAccessToken",
"providerAccessTokenExpiry",
"providerRefreshToken"
]
],
"example": {
"$id": "5e5ea5c16897e",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"$updatedAt": "2020-10-15T06:38:00.000+00:00",
"userId": "5e5bb8c16897e",
"provider": "email",
"providerUid": "5e5bb8c16897e",
"providerEmail": "user@example.com",
"providerAccessToken": "MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3",
"providerAccessTokenExpiry": "2020-10-15T06:38:00.000+00:00",
"providerRefreshToken": "MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3"
}
},
"token": {
"description": "Token",
@@ -9222,7 +9431,15 @@
"secret",
"expire",
"phrase"
]
],
"example": {
"$id": "bb8ea5c16897e",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"userId": "5e5ea5c168bb8",
"secret": "",
"expire": "2020-10-15T06:38:00.000+00:00",
"phrase": "Golden Fox"
}
},
"jwt": {
"description": "JWT",
@@ -9236,7 +9453,10 @@
},
"required": [
"jwt"
]
],
"example": {
"jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
}
},
"locale": {
"description": "Locale",
@@ -9286,7 +9506,16 @@
"continent",
"eu",
"currency"
]
],
"example": {
"ip": "127.0.0.1",
"countryCode": "US",
"country": "United States",
"continentCode": "NA",
"continent": "North America",
"eu": false,
"currency": "USD"
}
},
"localeCode": {
"description": "LocaleCode",
@@ -9306,7 +9535,11 @@
"required": [
"code",
"name"
]
],
"example": {
"code": "en-us",
"name": "US"
}
},
"file": {
"description": "File",
@@ -9388,7 +9621,22 @@
"sizeOriginal",
"chunksTotal",
"chunksUploaded"
]
],
"example": {
"$id": "5e5ea5c16897e",
"bucketId": "5e5ea5c16897e",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"$updatedAt": "2020-10-15T06:38:00.000+00:00",
"$permissions": [
"read(\"any\")"
],
"name": "Pink.png",
"signature": "5d529fd02b544198ae075bd57c1762bb",
"mimeType": "image\/png",
"sizeOriginal": 17890,
"chunksTotal": 17890,
"chunksUploaded": 17890
}
},
"team": {
"description": "Team",
@@ -9439,7 +9687,18 @@
"name",
"total",
"prefs"
]
],
"example": {
"$id": "5e5ea5c16897e",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"$updatedAt": "2020-10-15T06:38:00.000+00:00",
"name": "VIP",
"total": 7,
"prefs": {
"theme": "pink",
"timezone": "UTC"
}
}
},
"membership": {
"description": "Membership",
@@ -9530,7 +9789,24 @@
"confirm",
"mfa",
"roles"
]
],
"example": {
"$id": "5e5ea5c16897e",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"$updatedAt": "2020-10-15T06:38:00.000+00:00",
"userId": "5e5ea5c16897e",
"userName": "John Doe",
"userEmail": "john@appwrite.io",
"teamId": "5e5ea5c16897e",
"teamName": "VIP",
"invited": "2020-10-15T06:38:00.000+00:00",
"joined": "2020-10-15T06:38:00.000+00:00",
"confirm": false,
"mfa": false,
"roles": [
"owner"
]
}
},
"execution": {
"description": "Execution",
@@ -9661,7 +9937,36 @@
"logs",
"errors",
"duration"
]
],
"example": {
"$id": "5e5ea5c16897e",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"$updatedAt": "2020-10-15T06:38:00.000+00:00",
"$permissions": [
"any"
],
"functionId": "5e5ea6g16897e",
"trigger": "http",
"status": "processing",
"requestMethod": "GET",
"requestPath": "\/articles?id=5",
"requestHeaders": [
{
"Content-Type": "application\/json"
}
],
"responseStatusCode": 200,
"responseBody": "",
"responseHeaders": [
{
"Content-Type": "application\/json"
}
],
"logs": "",
"errors": "",
"duration": 0.4,
"scheduledAt": "2020-10-15T06:38:00.000+00:00"
}
},
"country": {
"description": "Country",
@@ -9681,7 +9986,11 @@
"required": [
"name",
"code"
]
],
"example": {
"name": "United States",
"code": "US"
}
},
"continent": {
"description": "Continent",
@@ -9701,7 +10010,11 @@
"required": [
"name",
"code"
]
],
"example": {
"name": "Europe",
"code": "EU"
}
},
"language": {
"description": "Language",
@@ -9727,7 +10040,12 @@
"name",
"code",
"nativeName"
]
],
"example": {
"name": "Italian",
"code": "it",
"nativeName": "Italiano"
}
},
"currency": {
"description": "Currency",
@@ -9779,7 +10097,16 @@
"rounding",
"code",
"namePlural"
]
],
"example": {
"symbol": "$",
"name": "US dollar",
"symbolNative": "$",
"decimalDigits": 2,
"rounding": 0,
"code": "USD",
"namePlural": "US dollars"
}
},
"phone": {
"description": "Phone",
@@ -9805,7 +10132,12 @@
"code",
"countryCode",
"countryName"
]
],
"example": {
"code": "+1",
"countryCode": "US",
"countryName": "United States"
}
},
"headers": {
"description": "Headers",
@@ -9825,7 +10157,11 @@
"required": [
"name",
"value"
]
],
"example": {
"name": "Content-Type",
"value": "application\/json"
}
},
"mfaChallenge": {
"description": "MFA Challenge",
@@ -9857,7 +10193,13 @@
"$createdAt",
"userId",
"expire"
]
],
"example": {
"$id": "bb8ea5c16897e",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"userId": "5e5ea5c168bb8",
"expire": "2020-10-15T06:38:00.000+00:00"
}
},
"mfaRecoveryCodes": {
"description": "MFA Recovery Codes",
@@ -9877,7 +10219,13 @@
},
"required": [
"recoveryCodes"
]
],
"example": {
"recoveryCodes": [
"a3kf0-s0cl2",
"s0co1-as98s"
]
}
},
"mfaType": {
"description": "MFAType",
@@ -9897,7 +10245,11 @@
"required": [
"secret",
"uri"
]
],
"example": {
"secret": true,
"uri": true
}
},
"mfaFactors": {
"description": "MFAFactors",
@@ -9929,7 +10281,13 @@
"phone",
"email",
"recoveryCode"
]
],
"example": {
"totp": true,
"phone": true,
"email": true,
"recoveryCode": true
}
},
"subscriber": {
"description": "Subscriber",
@@ -10003,7 +10361,27 @@
"userName",
"topicId",
"providerType"
]
],
"example": {
"$id": "259125845563242502",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"$updatedAt": "2020-10-15T06:38:00.000+00:00",
"targetId": "259125845563242502",
"target": {
"$id": "259125845563242502",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"$updatedAt": "2020-10-15T06:38:00.000+00:00",
"providerType": "email",
"providerId": "259125845563242502",
"name": "ageon-app-email",
"identifier": "random-mail@email.org",
"userId": "5e5ea5c16897e"
},
"userId": "5e5ea5c16897e",
"userName": "Aegon Targaryen",
"topicId": "259125845563242502",
"providerType": "email"
}
},
"target": {
"description": "Target",
@@ -10065,7 +10443,18 @@
"providerType",
"identifier",
"expired"
]
],
"example": {
"$id": "259125845563242502",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"$updatedAt": "2020-10-15T06:38:00.000+00:00",
"name": "Apple iPhone 12",
"userId": "259125845563242502",
"providerId": "259125845563242502",
"providerType": "email",
"identifier": "token",
"expired": false
}
}
},
"securitySchemes": {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+432 -49
View File
@@ -5461,7 +5461,7 @@
"type": "string",
"description": "Scheduled execution time in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. DateTime value must be in future with precision in minutes.",
"default": null,
"x-example": null
"x-example": "<SCHEDULED_AT>"
}
}
}
@@ -8094,7 +8094,8 @@
"any": {
"description": "Any",
"type": "object",
"additionalProperties": true
"additionalProperties": true,
"example": []
},
"documentList": {
"description": "Documents List",
@@ -8119,7 +8120,11 @@
"required": [
"total",
"documents"
]
],
"example": {
"total": 5,
"documents": ""
}
},
"sessionList": {
"description": "Sessions List",
@@ -8144,7 +8149,11 @@
"required": [
"total",
"sessions"
]
],
"example": {
"total": 5,
"sessions": ""
}
},
"identityList": {
"description": "Identities List",
@@ -8169,7 +8178,11 @@
"required": [
"total",
"identities"
]
],
"example": {
"total": 5,
"identities": ""
}
},
"logList": {
"description": "Logs List",
@@ -8194,7 +8207,11 @@
"required": [
"total",
"logs"
]
],
"example": {
"total": 5,
"logs": ""
}
},
"fileList": {
"description": "Files List",
@@ -8219,7 +8236,11 @@
"required": [
"total",
"files"
]
],
"example": {
"total": 5,
"files": ""
}
},
"teamList": {
"description": "Teams List",
@@ -8244,7 +8265,11 @@
"required": [
"total",
"teams"
]
],
"example": {
"total": 5,
"teams": ""
}
},
"membershipList": {
"description": "Memberships List",
@@ -8269,7 +8294,11 @@
"required": [
"total",
"memberships"
]
],
"example": {
"total": 5,
"memberships": ""
}
},
"executionList": {
"description": "Executions List",
@@ -8294,7 +8323,11 @@
"required": [
"total",
"executions"
]
],
"example": {
"total": 5,
"executions": ""
}
},
"countryList": {
"description": "Countries List",
@@ -8319,7 +8352,11 @@
"required": [
"total",
"countries"
]
],
"example": {
"total": 5,
"countries": ""
}
},
"continentList": {
"description": "Continents List",
@@ -8344,7 +8381,11 @@
"required": [
"total",
"continents"
]
],
"example": {
"total": 5,
"continents": ""
}
},
"languageList": {
"description": "Languages List",
@@ -8369,7 +8410,11 @@
"required": [
"total",
"languages"
]
],
"example": {
"total": 5,
"languages": ""
}
},
"currencyList": {
"description": "Currencies List",
@@ -8394,7 +8439,11 @@
"required": [
"total",
"currencies"
]
],
"example": {
"total": 5,
"currencies": ""
}
},
"phoneList": {
"description": "Phones List",
@@ -8419,7 +8468,11 @@
"required": [
"total",
"phones"
]
],
"example": {
"total": 5,
"phones": ""
}
},
"localeCodeList": {
"description": "Locale codes list",
@@ -8444,7 +8497,11 @@
"required": [
"total",
"localeCodes"
]
],
"example": {
"total": 5,
"localeCodes": ""
}
},
"document": {
"description": "Document",
@@ -8501,7 +8558,23 @@
"$createdAt",
"$updatedAt",
"$permissions"
]
],
"example": {
"$id": "5e5ea5c16897e",
"$sequence": 1,
"$collectionId": "5e5ea5c15117e",
"$databaseId": "5e5ea5c15117e",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"$updatedAt": "2020-10-15T06:38:00.000+00:00",
"$permissions": [
"read(\"any\")"
],
"username": "john.doe",
"email": "john.doe@example.com",
"fullName": "John Doe",
"age": 30,
"isAdmin": false
}
},
"log": {
"description": "Log",
@@ -8635,7 +8708,30 @@
"deviceModel",
"countryCode",
"countryName"
]
],
"example": {
"event": "account.sessions.create",
"userId": "610fc2f985ee0",
"userEmail": "john@appwrite.io",
"userName": "John Doe",
"mode": "admin",
"ip": "127.0.0.1",
"time": "2020-10-15T06:38:00.000+00:00",
"osCode": "Mac",
"osName": "Mac",
"osVersion": "Mac",
"clientType": "browser",
"clientCode": "CM",
"clientName": "Chrome Mobile iOS",
"clientVersion": "84.0",
"clientEngine": "WebKit",
"clientEngineVersion": "605.1.15",
"deviceName": "smartphone",
"deviceBrand": "Google",
"deviceModel": "Nexus 5",
"countryCode": "US",
"countryName": "United States"
}
},
"user": {
"description": "User",
@@ -8798,7 +8894,33 @@
"prefs",
"targets",
"accessedAt"
]
],
"example": {
"$id": "5e5ea5c16897e",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"$updatedAt": "2020-10-15T06:38:00.000+00:00",
"name": "John Doe",
"password": "$argon2id$v=19$m=2048,t=4,p=3$aUZjLnliVWRINmFNTWMudg$5S+x+7uA31xFnrHFT47yFwcJeaP0w92L\/4LdgrVRXxE",
"hash": "argon2",
"hashOptions": {},
"registration": "2020-10-15T06:38:00.000+00:00",
"status": true,
"labels": [
"vip"
],
"passwordUpdate": "2020-10-15T06:38:00.000+00:00",
"email": "john@appwrite.io",
"phone": "+4930901820",
"emailVerification": true,
"phoneVerification": true,
"mfa": true,
"prefs": {
"theme": "pink",
"timezone": "UTC"
},
"targets": [],
"accessedAt": "2020-10-15T06:38:00.000+00:00"
}
},
"algoMd5": {
"description": "AlgoMD5",
@@ -8812,7 +8934,10 @@
},
"required": [
"type"
]
],
"example": {
"type": "md5"
}
},
"algoSha": {
"description": "AlgoSHA",
@@ -8826,7 +8951,10 @@
},
"required": [
"type"
]
],
"example": {
"type": "sha"
}
},
"algoPhpass": {
"description": "AlgoPHPass",
@@ -8840,7 +8968,10 @@
},
"required": [
"type"
]
],
"example": {
"type": "phpass"
}
},
"algoBcrypt": {
"description": "AlgoBcrypt",
@@ -8854,7 +8985,10 @@
},
"required": [
"type"
]
],
"example": {
"type": "bcrypt"
}
},
"algoScrypt": {
"description": "AlgoScrypt",
@@ -8896,7 +9030,14 @@
"costMemory",
"costParallel",
"length"
]
],
"example": {
"type": "scrypt",
"costCpu": 8,
"costMemory": 14,
"costParallel": 1,
"length": 64
}
},
"algoScryptModified": {
"description": "AlgoScryptModified",
@@ -8928,7 +9069,13 @@
"salt",
"saltSeparator",
"signerKey"
]
],
"example": {
"type": "scryptMod",
"salt": "UxLMreBr6tYyjQ==",
"saltSeparator": "Bw==",
"signerKey": "XyEKE9RcTDeLEsL\/RjwPDBv\/RqDl8fb3gpYEOQaPihbxf1ZAtSOHCjuAAa7Q3oHpCYhXSN9tizHgVOwn6krflQ=="
}
},
"algoArgon2": {
"description": "AlgoArgon2",
@@ -8963,12 +9110,23 @@
"memoryCost",
"timeCost",
"threads"
]
],
"example": {
"type": "argon2",
"memoryCost": 65536,
"timeCost": 4,
"threads": 3
}
},
"preferences": {
"description": "Preferences",
"type": "object",
"additionalProperties": true
"additionalProperties": true,
"example": {
"language": "en",
"timezone": "UTC",
"darkTheme": true
}
},
"session": {
"description": "Session",
@@ -9155,7 +9313,40 @@
"factors",
"secret",
"mfaUpdatedAt"
]
],
"example": {
"$id": "5e5ea5c16897e",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"$updatedAt": "2020-10-15T06:38:00.000+00:00",
"userId": "5e5bb8c16897e",
"expire": "2020-10-15T06:38:00.000+00:00",
"provider": "email",
"providerUid": "user@example.com",
"providerAccessToken": "MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3",
"providerAccessTokenExpiry": "2020-10-15T06:38:00.000+00:00",
"providerRefreshToken": "MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3",
"ip": "127.0.0.1",
"osCode": "Mac",
"osName": "Mac",
"osVersion": "Mac",
"clientType": "browser",
"clientCode": "CM",
"clientName": "Chrome Mobile iOS",
"clientVersion": "84.0",
"clientEngine": "WebKit",
"clientEngineVersion": "605.1.15",
"deviceName": "smartphone",
"deviceBrand": "Google",
"deviceModel": "Nexus 5",
"countryCode": "US",
"countryName": "United States",
"current": true,
"factors": [
"email"
],
"secret": "5e5bb8c16897e",
"mfaUpdatedAt": "2020-10-15T06:38:00.000+00:00"
}
},
"identity": {
"description": "Identity",
@@ -9223,7 +9414,19 @@
"providerAccessToken",
"providerAccessTokenExpiry",
"providerRefreshToken"
]
],
"example": {
"$id": "5e5ea5c16897e",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"$updatedAt": "2020-10-15T06:38:00.000+00:00",
"userId": "5e5bb8c16897e",
"provider": "email",
"providerUid": "5e5bb8c16897e",
"providerEmail": "user@example.com",
"providerAccessToken": "MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3",
"providerAccessTokenExpiry": "2020-10-15T06:38:00.000+00:00",
"providerRefreshToken": "MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3"
}
},
"token": {
"description": "Token",
@@ -9267,7 +9470,15 @@
"secret",
"expire",
"phrase"
]
],
"example": {
"$id": "bb8ea5c16897e",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"userId": "5e5ea5c168bb8",
"secret": "",
"expire": "2020-10-15T06:38:00.000+00:00",
"phrase": "Golden Fox"
}
},
"jwt": {
"description": "JWT",
@@ -9281,7 +9492,10 @@
},
"required": [
"jwt"
]
],
"example": {
"jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
}
},
"locale": {
"description": "Locale",
@@ -9331,7 +9545,16 @@
"continent",
"eu",
"currency"
]
],
"example": {
"ip": "127.0.0.1",
"countryCode": "US",
"country": "United States",
"continentCode": "NA",
"continent": "North America",
"eu": false,
"currency": "USD"
}
},
"localeCode": {
"description": "LocaleCode",
@@ -9351,7 +9574,11 @@
"required": [
"code",
"name"
]
],
"example": {
"code": "en-us",
"name": "US"
}
},
"file": {
"description": "File",
@@ -9433,7 +9660,22 @@
"sizeOriginal",
"chunksTotal",
"chunksUploaded"
]
],
"example": {
"$id": "5e5ea5c16897e",
"bucketId": "5e5ea5c16897e",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"$updatedAt": "2020-10-15T06:38:00.000+00:00",
"$permissions": [
"read(\"any\")"
],
"name": "Pink.png",
"signature": "5d529fd02b544198ae075bd57c1762bb",
"mimeType": "image\/png",
"sizeOriginal": 17890,
"chunksTotal": 17890,
"chunksUploaded": 17890
}
},
"team": {
"description": "Team",
@@ -9485,7 +9727,18 @@
"name",
"total",
"prefs"
]
],
"example": {
"$id": "5e5ea5c16897e",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"$updatedAt": "2020-10-15T06:38:00.000+00:00",
"name": "VIP",
"total": 7,
"prefs": {
"theme": "pink",
"timezone": "UTC"
}
}
},
"membership": {
"description": "Membership",
@@ -9576,7 +9829,24 @@
"confirm",
"mfa",
"roles"
]
],
"example": {
"$id": "5e5ea5c16897e",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"$updatedAt": "2020-10-15T06:38:00.000+00:00",
"userId": "5e5ea5c16897e",
"userName": "John Doe",
"userEmail": "john@appwrite.io",
"teamId": "5e5ea5c16897e",
"teamName": "VIP",
"invited": "2020-10-15T06:38:00.000+00:00",
"joined": "2020-10-15T06:38:00.000+00:00",
"confirm": false,
"mfa": false,
"roles": [
"owner"
]
}
},
"execution": {
"description": "Execution",
@@ -9709,7 +9979,36 @@
"logs",
"errors",
"duration"
]
],
"example": {
"$id": "5e5ea5c16897e",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"$updatedAt": "2020-10-15T06:38:00.000+00:00",
"$permissions": [
"any"
],
"functionId": "5e5ea6g16897e",
"trigger": "http",
"status": "processing",
"requestMethod": "GET",
"requestPath": "\/articles?id=5",
"requestHeaders": [
{
"Content-Type": "application\/json"
}
],
"responseStatusCode": 200,
"responseBody": "",
"responseHeaders": [
{
"Content-Type": "application\/json"
}
],
"logs": "",
"errors": "",
"duration": 0.4,
"scheduledAt": "2020-10-15T06:38:00.000+00:00"
}
},
"country": {
"description": "Country",
@@ -9729,7 +10028,11 @@
"required": [
"name",
"code"
]
],
"example": {
"name": "United States",
"code": "US"
}
},
"continent": {
"description": "Continent",
@@ -9749,7 +10052,11 @@
"required": [
"name",
"code"
]
],
"example": {
"name": "Europe",
"code": "EU"
}
},
"language": {
"description": "Language",
@@ -9775,7 +10082,12 @@
"name",
"code",
"nativeName"
]
],
"example": {
"name": "Italian",
"code": "it",
"nativeName": "Italiano"
}
},
"currency": {
"description": "Currency",
@@ -9827,7 +10139,16 @@
"rounding",
"code",
"namePlural"
]
],
"example": {
"symbol": "$",
"name": "US dollar",
"symbolNative": "$",
"decimalDigits": 2,
"rounding": 0,
"code": "USD",
"namePlural": "US dollars"
}
},
"phone": {
"description": "Phone",
@@ -9853,7 +10174,12 @@
"code",
"countryCode",
"countryName"
]
],
"example": {
"code": "+1",
"countryCode": "US",
"countryName": "United States"
}
},
"headers": {
"description": "Headers",
@@ -9873,7 +10199,11 @@
"required": [
"name",
"value"
]
],
"example": {
"name": "Content-Type",
"value": "application\/json"
}
},
"mfaChallenge": {
"description": "MFA Challenge",
@@ -9905,7 +10235,13 @@
"$createdAt",
"userId",
"expire"
]
],
"example": {
"$id": "bb8ea5c16897e",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"userId": "5e5ea5c168bb8",
"expire": "2020-10-15T06:38:00.000+00:00"
}
},
"mfaRecoveryCodes": {
"description": "MFA Recovery Codes",
@@ -9925,7 +10261,13 @@
},
"required": [
"recoveryCodes"
]
],
"example": {
"recoveryCodes": [
"a3kf0-s0cl2",
"s0co1-as98s"
]
}
},
"mfaType": {
"description": "MFAType",
@@ -9945,7 +10287,11 @@
"required": [
"secret",
"uri"
]
],
"example": {
"secret": true,
"uri": true
}
},
"mfaFactors": {
"description": "MFAFactors",
@@ -9977,7 +10323,13 @@
"phone",
"email",
"recoveryCode"
]
],
"example": {
"totp": true,
"phone": true,
"email": true,
"recoveryCode": true
}
},
"subscriber": {
"description": "Subscriber",
@@ -10052,7 +10404,27 @@
"userName",
"topicId",
"providerType"
]
],
"example": {
"$id": "259125845563242502",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"$updatedAt": "2020-10-15T06:38:00.000+00:00",
"targetId": "259125845563242502",
"target": {
"$id": "259125845563242502",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"$updatedAt": "2020-10-15T06:38:00.000+00:00",
"providerType": "email",
"providerId": "259125845563242502",
"name": "ageon-app-email",
"identifier": "random-mail@email.org",
"userId": "5e5ea5c16897e"
},
"userId": "5e5ea5c16897e",
"userName": "Aegon Targaryen",
"topicId": "259125845563242502",
"providerType": "email"
}
},
"target": {
"description": "Target",
@@ -10114,7 +10486,18 @@
"providerType",
"identifier",
"expired"
]
],
"example": {
"$id": "259125845563242502",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"$updatedAt": "2020-10-15T06:38:00.000+00:00",
"name": "Apple iPhone 12",
"userId": "259125845563242502",
"providerId": "259125845563242502",
"providerType": "email",
"identifier": "token",
"expired": false
}
}
},
"externalDocs": {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+33 -9
View File
@@ -10,6 +10,7 @@ use Appwrite\URL\URL as URLParse;
use Appwrite\Utopia\Response;
use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions;
use enshrined\svgSanitize\Sanitizer as SvgSanitizer;
use Utopia\App;
use Utopia\Config\Config;
use Utopia\Database\Database;
@@ -362,7 +363,8 @@ App::get('/v1/avatars/favicon')
$client = new Client();
try {
$res = $client
->setAllowRedirects(false)
->setAllowRedirects(true)
->setMaxRedirects(5)
->setUserAgent(\sprintf(
APP_USERAGENT,
System::getEnv('_APP_VERSION', 'UNKNOWN'),
@@ -373,15 +375,11 @@ App::get('/v1/avatars/favicon')
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
}
if ($res->getStatusCode() !== 200) {
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
}
$doc = new DOMDocument();
$doc->strictErrorChecking = false;
@$doc->loadHTML($res->getBody());
$links = $doc->getElementsByTagName('link');
$links = $doc->getElementsByTagName('link') ?? [];
$outputHref = '';
$outputExt = '';
$space = 0;
@@ -399,6 +397,12 @@ App::get('/v1/avatars/favicon')
$ext = \pathinfo(\parse_url($absolute, PHP_URL_PATH), PATHINFO_EXTENSION);
switch ($ext) {
case 'svg':
// SVG icons are prioritized by assigning the maximum possible value.
$space = PHP_INT_MAX;
$outputHref = $absolute;
$outputExt = $ext;
break;
case 'ico':
case 'png':
case 'jpg':
@@ -437,7 +441,8 @@ App::get('/v1/avatars/favicon')
$client = new Client();
try {
$res = $client
->setAllowRedirects(false)
->setAllowRedirects(true)
->setMaxRedirects(5)
->fetch($outputHref);
} catch (\Throwable) {
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
@@ -449,14 +454,33 @@ App::get('/v1/avatars/favicon')
$data = $res->getBody();
if ('ico' == $outputExt) { // Skip crop, Imagick isn\'t supporting icon files
if (empty($data) || (\mb_substr($data, 0, 5) === '<html') || \mb_substr($data, 0, 5) === '<!doc') {
if ('ico' === $outputExt) { // Skip crop, Imagick isn\'t supporting icon files
if (
empty($data) ||
stripos($data, '<html') === 0 ||
stripos($data, '<!doc') === 0
) {
throw new Exception(Exception::AVATAR_ICON_NOT_FOUND, 'Favicon not found');
}
$response
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
->setContentType('image/x-icon')
->file($data);
return;
}
if ('svg' === $outputExt) { // Skip crop, Imagick isn\'t supporting svg files
$sanitizer = new SvgSanitizer();
$sanitizer->minify(true);
$cleanSvg = $sanitizer->sanitize($data);
if ($cleanSvg === false) {
throw new \Exception('SVG sanitization failed');
}
$response
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
->setContentType('image/svg+xml')
->file($cleanSvg);
return;
}
$image = new Image($data);
+4 -2
View File
@@ -1407,9 +1407,10 @@ App::get('/robots.txt')
->inject('apiKey')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) {
$host = $request->getHostname() ?? '';
$consoleDomain = System::getEnv('_APP_CONSOLE_DOMAIN', '');
$mainDomain = System::getEnv('_APP_DOMAIN', '');
if (($host === $mainDomain || $host === 'localhost') && empty($previewHostname)) {
if (($host === $consoleDomain || $host === $mainDomain || $host === 'localhost') && empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/robots.phtml');
$response->text($template->render(false));
} else {
@@ -1440,9 +1441,10 @@ App::get('/humans.txt')
->inject('apiKey')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) {
$host = $request->getHostname() ?? '';
$consoleDomain = System::getEnv('_APP_CONSOLE_DOMAIN', '');
$mainDomain = System::getEnv('_APP_DOMAIN', '');
if (($host === $mainDomain || $host === 'localhost') && empty($previewHostname)) {
if (($host === $consoleDomain || $host === $mainDomain || $host === 'localhost') && empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/humans.phtml');
$response->text($template->render(false));
} else {
+6 -1
View File
@@ -31,6 +31,7 @@ use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Queue\Publisher;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
use Utopia\Validator\WhiteList;
$parseLabel = function (string $label, array $responsePayload, array $requestParams, Document $user) {
@@ -421,7 +422,8 @@ App::init()
->inject('apiKey')
->inject('plan')
->inject('devKey')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Publisher $publisher, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey) use ($usageDatabaseListener, $eventDatabaseListener) {
->inject('telemetry')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Publisher $publisher, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry) use ($usageDatabaseListener, $eventDatabaseListener) {
$route = $utopia->getRoute();
@@ -554,6 +556,7 @@ App::init()
));
$useCache = $route->getLabel('cache', false);
$storageCacheOperationsCounter = $telemetry->createCounter('storage.cache.operations.load');
if ($useCache) {
$route = $utopia->match($request);
$isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview';
@@ -619,10 +622,12 @@ App::init()
->addHeader('Cache-Control', sprintf('private, max-age=%d', $timestamp))
->addHeader('X-Appwrite-Cache', 'hit')
->setContentType($cacheLog->getAttribute('mimeType'));
$storageCacheOperationsCounter->add(1, ['result' => 'hit']);
if (!$isImageTransformation || !$isDisabled) {
$response->send($data);
}
} else {
$storageCacheOperationsCounter->add(1, ['result' => 'miss']);
$response
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
->addHeader('Pragma', 'no-cache')
+4 -4
View File
@@ -52,7 +52,7 @@
"utopia-php/cache": "0.13.*",
"utopia-php/cli": "0.15.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "dev-feat-mongo-tmp as 0.73.0",
"utopia-php/database": "dev-feat-mongo-tmp as 0.75.0",
"utopia-php/detector": "0.1.*",
"utopia-php/domains": "0.8.*",
"utopia-php/dsn": "0.2.1",
@@ -62,8 +62,7 @@
"utopia-php/locale": "0.4.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.18.*",
"utopia-php/migration": "0.13.*",
"utopia-php/mongo": "0.5.*",
"utopia-php/migration": "0.14.*",
"utopia-php/orchestration": "0.9.*",
"utopia-php/platform": "0.7.*",
"utopia-php/pools": "0.8.*",
@@ -83,7 +82,8 @@
"adhocore/jwt": "1.1.*",
"spomky-labs/otphp": "^10.0",
"webonyx/graphql-php": "14.11.*",
"league/csv": "9.14.*"
"league/csv": "9.14.*",
"enshrined/svg-sanitize": "0.21.*"
},
"require-dev": {
"ext-fileinfo": "*",
+2 -1
View File
@@ -1,10 +1,11 @@
# Change Log
## 0.14.1
## 0.15.0
* Add `incrementDocumentAttribute` and `decrementDocumentAttribute` support to `Databases` service
* Add `encrypt` support to `StringAttribute` model
* Add `sequence` support to `Document` model
* Fix: pass enum value as string in API params
## 0.14.0
+2
View File
@@ -383,6 +383,8 @@ class Event
{
$this->params = [];
$this->sensitive = [];
$this->event = '';
$this->payload = [];
return $this;
}
+1
View File
@@ -89,6 +89,7 @@ abstract class Migration
'1.7.2' => 'V22',
'1.7.3' => 'V22',
'1.7.4' => 'V22',
'1.8.0' => 'V23'
];
/**
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace Appwrite\Migration\Version;
use Appwrite\Migration\Migration;
use Exception;
use Throwable;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\Document;
class V23 extends Migration
{
/**
* @throws Throwable
*/
public function execute(): void
{
/**
* Disable SubQueries for Performance.
*/
foreach (['subQueryIndexes', 'subQueryPlatforms', 'subQueryDomains', 'subQueryKeys', 'subQueryDevKeys', 'subQueryWebhooks', 'subQuerySessions', 'subQueryTokens', 'subQueryMemberships', 'subQueryVariables', 'subQueryChallenges', 'subQueryProjectVariables', 'subQueryTargets', 'subQueryTopicTargets'] as $name) {
Database::addFilter(
$name,
fn () => null,
fn () => []
);
}
Console::info('Migrating databases');
$this->migrateDatabases();
}
/**
* Migrate Databases.
*
* @return void
* @throws Exception|Throwable
*/
private function migrateDatabases(): void
{
if ($this->project->getId() === 'console') {
return;
}
// since required + default can't be used together
// so first creating the attribute then bulk updating the attribute
$this->createAttributeFromCollection($this->dbForProject, 'databases', 'type');
$this->dbForProject->updateDocuments('databases', new Document(['type' => 'legacy']));
}
}
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Utopia\Database\Database;
use Utopia\Database\Document;
@@ -279,4 +280,59 @@ abstract class Action extends UtopiaAction
return true;
}
/**
* For triggering different queues for each document for a bulk documents
* @param string $event
* @param Document $database
* @param Document $collection
* @param Document[] $documents
* @param Event $queueForEvents
* @param Event $queueForRealtime
* @param Event $queueForFunctions
* @param Event $queueForWebhooks
* @return void
*/
protected function triggerBulk(
string $event,
Document $database,
Document $collection,
array $documents,
Event $queueForEvents,
Event $queueForRealtime,
Event $queueForFunctions,
Event $queueForWebhooks
): void {
$queueForEvents
->setEvent($event)
->setParam('databaseId', $database->getId())
->setContext('database', $database)
->setParam('collectionId', $collection->getId())
->setParam('tableId', $collection->getId())
->setContext($this->getCollectionsEventsContext(), $collection);
foreach ($documents as $document) {
$queueForEvents
->setParam('documentId', $document->getId())
->setParam('rowId', $document->getId())
->setPayload($document->getArrayCopy());
$queueForRealtime
->from($queueForEvents)
->trigger();
$queueForFunctions
->from($queueForEvents)
->trigger();
$queueForWebhooks
->from($queueForEvents)
->trigger();
}
$queueForEvents->reset();
$queueForRealtime->reset();
$queueForFunctions->reset();
$queueForWebhooks->reset();
}
}
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk;
use Appwrite\Event\Event;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
@@ -72,11 +73,15 @@ class Delete extends Action
->inject('response')
->inject('dbForProject')
->inject('queueForStatsUsage')
->inject('queueForEvents')
->inject('queueForRealtime')
->inject('queueForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, array $queries, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, array $plan): void
public function action(string $databaseId, string $collectionId, array $queries, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void
{
$database = $dbForProject->getDocument('databases', $databaseId);
if ($database->isEmpty()) {
@@ -134,5 +139,16 @@ class Delete extends Action
'total' => $modified,
$this->getSdkGroup() => $documents,
]), $this->getResponseModel());
$this->triggerBulk(
'databases.[databaseId].collections.[collectionId].documents.[documentId].delete',
$database,
$collection,
$documents,
$queueForEvents,
$queueForRealtime,
$queueForFunctions,
$queueForWebhooks
);
}
}
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk;
use Appwrite\Event\Event;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
@@ -76,11 +77,15 @@ class Update extends Action
->inject('response')
->inject('dbForProject')
->inject('queueForStatsUsage')
->inject('queueForEvents')
->inject('queueForRealtime')
->inject('queueForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string|array $data, array $queries, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, array $plan): void
public function action(string $databaseId, string $collectionId, string|array $data, array $queries, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void
{
$data = \is_string($data)
? \json_decode($data, true)
@@ -158,5 +163,16 @@ class Update extends Action
'total' => $modified,
$this->getSdkGroup() => $documents
]), $this->getResponseModel());
$this->triggerBulk(
'databases.[databaseId].collections.[collectionId].documents.[documentId].update',
$database,
$collection,
$documents,
$queueForEvents,
$queueForRealtime,
$queueForFunctions,
$queueForWebhooks
);
}
}
@@ -122,10 +122,12 @@ class Create extends Action
->inject('user')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->inject('queueForRealtime')
->inject('queueForFunctions')
->inject('queueForWebhooks')
->callback($this->action(...));
}
public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, UtopiaResponse $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage): void
public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, UtopiaResponse $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks): void
{
$data = \is_string($data)
? \json_decode($data, true)
@@ -417,6 +419,16 @@ class Create extends Action
$this->getSdkGroup() => $documents
]), $this->getBulkResponseModel());
$this->triggerBulk(
'databases.[databaseId].collections.[collectionId].documents.[documentId].create',
$database,
$collection,
$documents,
$queueForEvents,
$queueForRealtime,
$queueForFunctions,
$queueForWebhooks
);
return;
}
@@ -23,6 +23,7 @@ use Utopia\Platform\Action;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
class Create extends Action
{
@@ -80,13 +81,14 @@ class Create extends Action
->param('databaseId', '', new CustomId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('name', '', new Text(128), 'Database name. Max length: 128 chars.')
->param('enabled', true, new Boolean(), 'Is the database enabled? When set to \'disabled\', users cannot access the database but Server SDKs with an API key can still read and write to the database. No data is lost when this is toggled.', true)
->param('type', 'grids', new WhiteList(['grids','legacy']), 'Database type.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $name, bool $enabled, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
public function action(string $databaseId, string $name, bool $enabled, string $type, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
{
$databaseId = $databaseId == 'unique()' ? ID::unique() : $databaseId;
@@ -96,6 +98,7 @@ class Create extends Action
'name' => $name,
'enabled' => $enabled,
'search' => implode(' ', [$databaseId, $name]),
'type' => $type
]));
} catch (DuplicateException) {
throw new Exception(Exception::DATABASE_ALREADY_EXISTS);
@@ -59,6 +59,10 @@ class Delete extends DocumentsDelete
->inject('response')
->inject('dbForProject')
->inject('queueForStatsUsage')
->inject('queueForEvents')
->inject('queueForRealtime')
->inject('queueForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->callback($this->action(...));
}
@@ -61,6 +61,10 @@ class Update extends DocumentsUpdate
->inject('response')
->inject('dbForProject')
->inject('queueForStatsUsage')
->inject('queueForEvents')
->inject('queueForRealtime')
->inject('queueForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->callback($this->action(...));
}
@@ -101,6 +101,9 @@ class Create extends DocumentCreate
->inject('user')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->inject('queueForRealtime')
->inject('queueForFunctions')
->inject('queueForWebhooks')
->callback($this->action(...));
}
}
@@ -97,8 +97,8 @@ class Create extends Action
$deviceForFunctions->transfer($path, $destination, $deviceForFunctions);
$deployment->removeAttribute('$sequence');
$deployment = $dbForProject->createDocument('deployments', $deployment->setAttributes([
'$sequence' => '',
'$id' => $deploymentId,
'sourcePath' => $destination,
'totalSize' => $deployment->getAttribute('sourceSize', 0),
@@ -111,8 +111,8 @@ class Create extends Action
}
$deployment->removeAttribute('$sequence');
$deployment = $dbForProject->createDocument('deployments', $deployment->setAttributes([
'$sequence' => '',
'$id' => $deploymentId,
'sourcePath' => $destination,
'totalSize' => $deployment->getAttribute('sourceSize', 0),
@@ -122,6 +122,19 @@ class Maintenance extends Action
Console::info("[{$time}] Found " . \count($certificates) . " certificates for renewal, scheduling jobs.");
foreach ($certificates as $certificate) {
$domain = $certificate->getAttribute('domain');
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$rule = $dbForPlatform->getDocument('rules', md5($domain));
} else {
$rule = $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain]),
]);
}
if ($rule->isEmpty() || $rule->getAttribute('region') !== System::getEnv('_APP_REGION', 'default')) {
continue;
}
$queueForCertificate
->setDomain(new Document([
'domain' => $certificate->getAttribute('domain')
+1 -1
View File
@@ -119,7 +119,7 @@ class SDKs extends Action
$gettingStarted = ($gettingStarted) ? \file_get_contents($gettingStarted) : '';
$examples = \realpath(__DIR__ . '/../../../../docs/sdks/' . $language['key'] . '/EXAMPLES.md');
$examples = ($examples) ? \file_get_contents($examples) : '';
$changelog = \realpath(__DIR__ . '/../../../../docs/sdks/' . $language['key'] . '/CHANGELOG.md');
$changelog = $language['changelog'] ?? '';
$changelog = ($changelog) ? \file_get_contents($changelog) : '# Change Log';
$warning = '**This SDK is compatible with Appwrite server version ' . $version . '. For older versions, please check [previous releases](' . $language['url'] . '/releases).**';
$license = 'BSD-3-Clause';
@@ -632,6 +632,7 @@ class OpenAPI3 extends Format
$required = $model->getRequired();
$rules = $model->getRules();
$examples = [];
$output['components']['schemas'][$model->getType()] = [
'description' => $model->getName(),
@@ -655,6 +656,8 @@ class OpenAPI3 extends Format
$format = null;
$items = null;
$examples[$name] = $rule['example'] ?? null;
switch ($rule['type']) {
case 'string':
case 'datetime':
@@ -743,6 +746,12 @@ class OpenAPI3 extends Format
$output['components']['schemas'][$model->getType()]['properties'][$name]['nullable'] = true;
}
}
if ($model->isAny() && !empty($model->getSampleData())) {
$examples = array_merge($examples, $model->getSampleData());
}
$output['components']['schemas'][$model->getType()]['example'] = $examples;
}
\ksort($output['paths']);
@@ -642,6 +642,7 @@ class Swagger2 extends Format
$required = $model->getRequired();
$rules = $model->getRules();
$examples = [];
$output['definitions'][$model->getType()] = [
'description' => $model->getName(),
@@ -665,6 +666,8 @@ class Swagger2 extends Format
$format = null;
$items = null;
$examples[$name] = $rule['example'] ?? null;
switch ($rule['type']) {
case 'string':
case 'datetime':
@@ -762,6 +765,12 @@ class Swagger2 extends Format
$output['definitions'][$model->getType()]['properties'][$name]['x-nullable'] = true;
}
}
if ($model->isAny() && !empty($model->getSampleData())) {
$examples = array_merge($examples, $model->getSampleData());
}
$output['definitions'][$model->getType()]['example'] = $examples;
}
\ksort($output['paths']);
@@ -31,4 +31,14 @@ class Any extends Model
{
return Response::MODEL_ANY;
}
/**
* Get sample data
*
* @return array
*/
public function getSampleData(): array
{
return [];
}
}
@@ -40,6 +40,12 @@ class Database extends Model
'default' => true,
'example' => false,
])
->addRule('type', [
'type' => self::TYPE_STRING,
'description' => 'Database type.',
'default' => 'grids',
'example' => 'grids',
])
;
}
@@ -95,4 +95,15 @@ class Document extends Any
return $document;
}
public function getSampleData(): array
{
return [
'username' => 'john.doe',
'email' => 'john.doe@example.com',
'fullName' => 'John Doe',
'age' => 30,
'isAdmin' => false,
];
}
}
@@ -25,4 +25,13 @@ class Preferences extends Any
{
return Response::MODEL_PREFERENCES;
}
public function getSampleData(): array
{
return [
'language' => 'en',
'timezone' => 'UTC',
'darkTheme' => true,
];
}
}
+2 -2
View File
@@ -182,9 +182,9 @@ abstract class Scope extends TestCase
/**
* @return array
*/
public function getUser(): array
public function getUser(bool $fresh = false): array
{
if (isset(self::$user[$this->getProject()['$id']])) {
if (!$fresh && isset(self::$user[$this->getProject()['$id']])) {
return self::$user[$this->getProject()['$id']];
}
+11 -1
View File
@@ -284,7 +284,17 @@ trait AvatarsBase
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('image/x-icon', $response['headers']['content-type']);
$this->assertEquals('image/svg+xml', $response['headers']['content-type']);
$this->assertNotEmpty($response['body']);
$response = $this->client->call(Client::METHOD_GET, '/avatars/favicon', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io/',
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('image/svg+xml', $response['headers']['content-type']);
$this->assertNotEmpty($response['body']);
/**
@@ -32,6 +32,44 @@ trait DatabasesBase
$this->assertNotEmpty($database['body']['$id']);
$this->assertEquals(201, $database['headers']['status-code']);
$this->assertEquals('Test Database', $database['body']['name']);
$this->assertEquals('grids', $database['body']['type']);
// testing to create a database with type
$database2 = $this->client->call(Client::METHOD_POST, '/databases', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'databaseId' => ID::unique(),
'name' => 'Test Database with type',
'type' => 'mongodb'
]);
$this->assertEquals(400, $database2['headers']['status-code']);
$database2 = $this->client->call(Client::METHOD_POST, '/databases', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'databaseId' => ID::unique(),
'name' => 'Test Database with type',
'type' => 'legacy'
]);
$this->assertNotEmpty($database2['body']['$id']);
$this->assertEquals(201, $database2['headers']['status-code']);
$this->assertEquals('Test Database with type', $database2['body']['name']);
$this->assertEquals('legacy', $database2['body']['type']);
// cleanup(for database2)
$databaseId = $database2['body']['$id'];
$response = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(204, $response['headers']['status-code']);
return ['databaseId' => $database['body']['$id']];
}
@@ -32,6 +32,44 @@ trait DatabasesBase
$this->assertNotEmpty($database['body']['$id']);
$this->assertEquals(201, $database['headers']['status-code']);
$this->assertEquals('Test Database', $database['body']['name']);
$this->assertEquals('grids', $database['body']['type']);
// testing to create a database with type
$database2 = $this->client->call(Client::METHOD_POST, '/databases', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'databaseId' => ID::unique(),
'name' => 'Test Database with type',
'type' => 'mongodb'
]);
$this->assertEquals(400, $database2['headers']['status-code']);
$database2 = $this->client->call(Client::METHOD_POST, '/databases', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'databaseId' => ID::unique(),
'name' => 'Test Database with type',
'type' => 'legacy'
]);
$this->assertNotEmpty($database2['body']['$id']);
$this->assertEquals(201, $database2['headers']['status-code']);
$this->assertEquals('Test Database with type', $database2['body']['name']);
$this->assertEquals('legacy', $database2['body']['type']);
// cleanup(for database2)
$databaseId = $database2['body']['$id'];
$response = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(204, $response['headers']['status-code']);
return ['databaseId' => $database['body']['$id']];
}
@@ -5700,6 +5738,26 @@ trait DatabasesBase
]), ['min' => 7]);
$this->assertEquals(400, $err['headers']['status-code']);
// Test min limit exceeded with custom value
$err = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId . '/count/decrement', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'value' => 3,
'min' => 5,
]);
$this->assertEquals(400, $err['headers']['status-code']);
// Test min limit 0
$err = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId . '/count/decrement', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'value' => 10,
'min' => 0,
]);
$this->assertEquals(400, $err['headers']['status-code']);
// Test type error on non-numeric attribute
$typeErr = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId . '/count/decrement', array_merge([
'content-type' => 'application/json',
@@ -900,9 +900,9 @@ trait MigrationsBase
/**
* Import documents from a CSV file.
*/
public function testCreateCsvMigration(): array
public function testCreateCsvMigration(): void
{
// make a database
// Make a database
$response = $this->client->call(Client::METHOD_POST, '/databases', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@@ -991,6 +991,7 @@ trait MigrationsBase
'missing-row' => $bucketOneId,
'missing-column' => $bucketOneId,
'irrelevant-column' => $bucketOneId,
'documents-internals' => $bucketOneId,
];
$fileIds = [];
@@ -999,7 +1000,8 @@ trait MigrationsBase
$csvFileName = match ($label) {
'missing-row',
'missing-column',
'irrelevant-column' => "{$label}.csv",
'irrelevant-column',
'documents-internals' => "{$label}.csv",
default => 'documents.csv',
};
@@ -1123,7 +1125,7 @@ trait MigrationsBase
);
}, 60000, 500);
// all data exists, pass/
// all data exists, pass.
$migration = $this->performCsvMigration(
[
'endpoint' => 'http://localhost/v1',
@@ -1133,32 +1135,8 @@ trait MigrationsBase
]
);
$this->assertEmpty($migration['body']['statusCounters']);
$this->assertEquals('CSV', $migration['body']['source']);
$this->assertEquals('pending', $migration['body']['status']);
$this->assertEquals('Appwrite', $migration['body']['destination']);
$this->assertContains(Resource::TYPE_ROW, $migration['body']['resources']);
return [
'databaseId' => $databaseId,
'tableId' => $tableId,
'migrationId' => $migration['body']['$id'],
];
}
/**
* @depends testCreateCsvMigration
*/
public function testImportSuccessful(array $response): void
{
$tableId = $response['tableId'];
$databaseId = $response['databaseId'];
$migrationId = $response['migrationId'];
$rowsCountInCSV = 100;
// get migration stats
$this->assertEventually(function () use ($migrationId, $databaseId, $tableId, $rowsCountInCSV) {
$this->assertEventually(function () use ($migration, $databaseId, $tableId) {
$migrationId = $migration['body']['$id'];
$migration = $this->client->call(Client::METHOD_GET, '/migrations/'.$migrationId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@@ -1171,8 +1149,8 @@ trait MigrationsBase
$this->assertEquals('Appwrite', $migration['body']['destination']);
$this->assertContains(Resource::TYPE_ROW, $migration['body']['resources']);
$this->assertArrayHasKey(Resource::TYPE_ROW, $migration['body']['statusCounters']);
$this->assertEquals($rowsCountInCSV, $migration['body']['statusCounters'][Resource::TYPE_ROW]['success']);
}, 1000, 500);
$this->assertEquals(100, $migration['body']['statusCounters'][Resource::TYPE_ROW]['success']);
}, 10_000, 500);
// get rows count
$rows = $this->client->call(Client::METHOD_GET, '/databases/'.$databaseId.'/grids/tables/'.$tableId.'/rows', array_merge([
@@ -1180,7 +1158,6 @@ trait MigrationsBase
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
// there should be only 100!
Query::limit(150)->toString()
]
]);
@@ -1188,7 +1165,34 @@ trait MigrationsBase
$this->assertEquals(200, $rows['headers']['status-code']);
$this->assertIsArray($rows['body']['rows']);
$this->assertIsNumeric($rows['body']['total']);
$this->assertEquals($rowsCountInCSV, $rows['body']['total']);
$this->assertEquals(100, $rows['body']['total']);
// all data exists and includes internals, pass.
$migration = $this->performCsvMigration(
[
'endpoint' => 'http://localhost/v1',
'fileId' => $fileIds['documents-internals'],
'bucketId' => $bucketIds['documents-internals'],
'resourceId' => $databaseId . ':' . $tableId,
]
);
$this->assertEventually(function () use ($migration, $databaseId, $tableId) {
$migrationId = $migration['body']['$id'];
$migration = $this->client->call(Client::METHOD_GET, '/migrations/'.$migrationId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $migration['headers']['status-code']);
$this->assertEquals('finished', $migration['body']['stage']);
$this->assertEquals('completed', $migration['body']['status']);
$this->assertEquals('CSV', $migration['body']['source']);
$this->assertEquals('Appwrite', $migration['body']['destination']);
$this->assertContains(Resource::TYPE_ROW, $migration['body']['resources']);
$this->assertArrayHasKey(Resource::TYPE_ROW, $migration['body']['statusCounters']);
$this->assertEquals(25, $migration['body']['statusCounters'][Resource::TYPE_ROW]['success']);
}, 10_000, 500);
}
private function performCsvMigration(array $body): array
@@ -3,6 +3,7 @@
namespace Tests\E2E\Services\Realtime;
use CURLFile;
use Exception;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
@@ -12,6 +13,7 @@ use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use WebSocket\ConnectionException;
use WebSocket\TimeoutException;
class RealtimeCustomClientTest extends Scope
{
@@ -819,7 +821,6 @@ class RealtimeCustomClientTest extends Scope
]);
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
@@ -898,9 +899,755 @@ class RealtimeCustomClientTest extends Scope
$this->assertNotEmpty($response['data']['payload']);
$this->assertEquals('Bradley Cooper', $response['data']['payload']['name']);
// test bulk create
$documents = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$actorsId}/documents", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'documents' => [
[
'$id' => ID::unique(),
'name' => 'Robert Downey Jr.',
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
],
[
'$id' => ID::unique(),
'name' => 'Scarlett Johansson',
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]
],
]);
// Receive first document event
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(6, $response['data']['channels']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.create", $response['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*.create", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*.create", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
$this->assertContains("databases.*.collections.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*", $response['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response['data']['events']);
$this->assertContains("databases.*", $response['data']['events']);
$this->assertNotEmpty($response['data']['payload']);
$this->assertIsArray($response['data']['payload']);
$this->assertArrayHasKey('$id', $response['data']['payload']);
$this->assertArrayHasKey('name', $response['data']['payload']);
$this->assertArrayHasKey('$permissions', $response['data']['payload']);
$this->assertIsArray($response['data']['payload']['$permissions']);
$this->assertContains(Permission::read(Role::any()), $response['data']['payload']['$permissions']);
$this->assertContains(Permission::update(Role::any()), $response['data']['payload']['$permissions']);
$this->assertContains(Permission::delete(Role::any()), $response['data']['payload']['$permissions']);
// Receive second document event
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(6, $response['data']['channels']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.create", $response['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*.create", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*.create", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
$this->assertContains("databases.*.collections.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*", $response['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response['data']['events']);
$this->assertContains("databases.*", $response['data']['events']);
$this->assertNotEmpty($response['data']['payload']);
$this->assertIsArray($response['data']['payload']);
$this->assertArrayHasKey('$id', $response['data']['payload']);
$this->assertArrayHasKey('name', $response['data']['payload']);
$this->assertArrayHasKey('$permissions', $response['data']['payload']);
$this->assertIsArray($response['data']['payload']['$permissions']);
$this->assertContains(Permission::read(Role::any()), $response['data']['payload']['$permissions']);
$this->assertContains(Permission::update(Role::any()), $response['data']['payload']['$permissions']);
$this->assertContains(Permission::delete(Role::any()), $response['data']['payload']['$permissions']);
// test bulk update
$response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $actorsId . '/documents/', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'data' => [
'name' => 'Marvel Hero',
'$permissions' => [
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
]
],
]);
$this->assertEquals(200, $response['headers']['status-code']);
// Receive first document update event
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(6, $response['data']['channels']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.update", $response['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*.update", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*.update", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
$this->assertContains("databases.*.collections.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*", $response['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response['data']['events']);
$this->assertContains("databases.*", $response['data']['events']);
$this->assertNotEmpty($response['data']['payload']);
$this->assertIsArray($response['data']['payload']);
$this->assertArrayHasKey('$id', $response['data']['payload']);
$this->assertEquals('Marvel Hero', $response['data']['payload']['name']);
$this->assertArrayHasKey('$permissions', $response['data']['payload']);
// Receive second document update event
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(6, $response['data']['channels']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.update", $response['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*.update", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*.update", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
$this->assertContains("databases.*.collections.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*", $response['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response['data']['events']);
$this->assertContains("databases.*", $response['data']['events']);
$this->assertNotEmpty($response['data']['payload']);
$this->assertIsArray($response['data']['payload']);
$this->assertArrayHasKey('$id', $response['data']['payload']);
$this->assertEquals('Marvel Hero', $response['data']['payload']['name']);
$this->assertArrayHasKey('$permissions', $response['data']['payload']);
// Receive third document update event
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(6, $response['data']['channels']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.update", $response['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*.update", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*.update", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
$this->assertContains("databases.*.collections.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*", $response['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response['data']['events']);
$this->assertContains("databases.*", $response['data']['events']);
$this->assertNotEmpty($response['data']['payload']);
$this->assertIsArray($response['data']['payload']);
$this->assertArrayHasKey('$id', $response['data']['payload']);
$this->assertEquals('Marvel Hero', $response['data']['payload']['name']);
$this->assertArrayHasKey('$permissions', $response['data']['payload']);
// Test bulk delete
$response = $this->client->call(Client::METHOD_DELETE, "/databases/{$databaseId}/collections/{$actorsId}/documents", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(200, $response['headers']['status-code']);
// Receive first document delete event
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(6, $response['data']['channels']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.delete", $response['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*.delete", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*.delete", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
$this->assertContains("databases.*.collections.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*", $response['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']);
$this->assertContains("databases.*", $response['data']['events']);
$this->assertNotEmpty($response['data']['payload']);
$this->assertIsArray($response['data']['payload']);
$this->assertArrayHasKey('$id', $response['data']['payload']);
$this->assertArrayHasKey('name', $response['data']['payload']);
$this->assertArrayHasKey('$permissions', $response['data']['payload']);
$this->assertIsArray($response['data']['payload']['$permissions']);
// Receive second document delete event
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(6, $response['data']['channels']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.delete", $response['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*.delete", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*.delete", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
$this->assertContains("databases.*.collections.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*", $response['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']);
$this->assertContains("databases.*", $response['data']['events']);
$this->assertNotEmpty($response['data']['payload']);
$this->assertIsArray($response['data']['payload']);
$this->assertArrayHasKey('$id', $response['data']['payload']);
$this->assertArrayHasKey('name', $response['data']['payload']);
$this->assertArrayHasKey('$permissions', $response['data']['payload']);
$this->assertIsArray($response['data']['payload']['$permissions']);
// Receive third document delete event
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(6, $response['data']['channels']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.delete", $response['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*.delete", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*.delete", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
$this->assertContains("databases.*.collections.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*", $response['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']);
$this->assertContains("databases.*", $response['data']['events']);
$this->assertNotEmpty($response['data']['payload']);
$this->assertIsArray($response['data']['payload']);
$this->assertArrayHasKey('$id', $response['data']['payload']);
$this->assertArrayHasKey('name', $response['data']['payload']);
$this->assertArrayHasKey('$permissions', $response['data']['payload']);
$this->assertIsArray($response['data']['payload']['$permissions']);
$client->close();
}
public function testChannelDatabaseBulkOperationMultipleClient()
{
// user with api key will do operations and other valid users
$user1 = $this->getUser(true);
$user1Id = $user1['$id'];
$session = $user1['session'] ?? '';
$projectId = $this->getProject()['$id'];
$client1 = $this->getWebsocket(['documents', 'collections'], [
'origin' => 'http://localhost',
'cookie' => 'a_session_' . $projectId . '=' . $session
]);
$response = json_decode($client1->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('connected', $response['type']);
$this->assertNotEmpty($response['data']);
$user2 = $this->getUser(true);
$user2Id = $user2['$id'];
$session = $user2['session'] ?? '';
$projectId = $this->getProject()['$id'];
$client2 = $this->getWebsocket(['documents', 'collections'], [
'origin' => 'http://localhost',
'cookie' => 'a_session_' . $projectId . '=' . $session
]);
$response = json_decode($client2->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('connected', $response['type']);
$this->assertNotEmpty($response['data']);
/**
* Test Database Create
*/
$database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'Actors DB',
]);
$databaseId = $database['body']['$id'];
/**
* Test Collection Create
*/
$actors = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'Actors',
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'documentSecurity' => true,
]);
$actorsId = $actors['body']['$id'];
$name = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $actorsId . '/attributes/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'name',
'size' => 256,
'required' => true,
]);
$this->assertEquals(202, $name['headers']['status-code']);
$this->assertEquals('name', $name['body']['key']);
$this->assertEquals('string', $name['body']['type']);
$this->assertEquals(256, $name['body']['size']);
$this->assertTrue($name['body']['required']);
sleep(2);
// create
$this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$actorsId}/documents", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'documents' => [
[
'$id' => ID::unique(),
'name' => 'Any',
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
],
[
'$id' => ID::unique(),
'name' => 'Users',
'$permissions' => [
Permission::read(Role::users()),
Permission::update(Role::users()),
Permission::delete(Role::users()),
],
],
[
'$id' => ID::unique(),
'name' => 'User1',
'$permissions' => [
Permission::read(Role::user($user1Id)),
],
],
[
'$id' => ID::unique(),
'name' => 'User2',
'$permissions' => [
Permission::read(Role::user($user2Id)),
],
],
[
'$id' => ID::unique(),
'name' => 'User2',
'$permissions' => [
Permission::read(Role::user($user2Id)),
],
]
],
]);
// Receive and assert for client1 - should receive 3 individual document events
for ($i = 0; $i < 3; $i++) {
$response1 = json_decode($client1->receive(), true);
$this->assertArrayHasKey('type', $response1);
$this->assertArrayHasKey('data', $response1);
$this->assertEquals('event', $response1['type']);
$this->assertNotEmpty($response1['data']);
$this->assertArrayHasKey('timestamp', $response1['data']);
$this->assertCount(6, $response1['data']['channels']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response1['data']['payload']['$id']}.create", $response1['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*.create", $response1['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response1['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*.create", $response1['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response1['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*", $response1['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response1['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response1['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response1['data']['events']);
$this->assertContains("databases.*.collections.*", $response1['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*", $response1['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}", $response1['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response1['data']['events']);
$this->assertContains("databases.*", $response1['data']['events']);
$this->assertNotEmpty($response1['data']['payload']);
$this->assertIsArray($response1['data']['payload']);
$this->assertArrayHasKey('$id', $response1['data']['payload']);
$this->assertArrayHasKey('name', $response1['data']['payload']);
$this->assertArrayHasKey('$permissions', $response1['data']['payload']);
$this->assertIsArray($response1['data']['payload']['$permissions']);
}
// Receive and assert for client2 - should receive 4 individual document events
for ($i = 0; $i < 4; $i++) {
$response2 = json_decode($client2->receive(), true);
$this->assertArrayHasKey('type', $response2);
$this->assertArrayHasKey('data', $response2);
$this->assertEquals('event', $response2['type']);
$this->assertNotEmpty($response2['data']);
$this->assertArrayHasKey('timestamp', $response2['data']);
$this->assertCount(6, $response2['data']['channels']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response2['data']['payload']['$id']}.create", $response2['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*.create", $response2['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response2['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*.create", $response2['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response2['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*", $response2['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response2['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response2['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response2['data']['events']);
$this->assertContains("databases.*.collections.*", $response2['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*", $response2['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}", $response2['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response2['data']['events']);
$this->assertContains("databases.*", $response2['data']['events']);
$this->assertNotEmpty($response2['data']['payload']);
$this->assertIsArray($response2['data']['payload']);
$this->assertArrayHasKey('$id', $response2['data']['payload']);
$this->assertArrayHasKey('name', $response2['data']['payload']);
$this->assertArrayHasKey('$permissions', $response2['data']['payload']);
$this->assertIsArray($response2['data']['payload']['$permissions']);
}
// Perform bulk update(making it only accessible by user1)
$response = $this->client->call(Client::METHOD_PATCH, "/databases/{$databaseId}/collections/{$actorsId}/documents/", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'data' => [
'name' => 'Marvel Hero',
'$permissions' => [
Permission::read(Role::user($user1Id)),
Permission::update(Role::user($user1Id)),
Permission::delete(Role::user($user1Id)),
]
],
]);
$this->assertEquals(200, $response['headers']['status-code']);
// Receive and assert for client1
for ($i = 0; $i < 5; $i++) {
$response1 = json_decode($client1->receive(), true);
$this->assertArrayHasKey('type', $response1);
$this->assertArrayHasKey('data', $response1);
$this->assertEquals('event', $response1['type']);
$this->assertNotEmpty($response1['data']);
$this->assertArrayHasKey('timestamp', $response1['data']);
$this->assertCount(6, $response1['data']['channels']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response1['data']['payload']['$id']}.update", $response1['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*.update", $response1['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response1['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*.update", $response1['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response1['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*", $response1['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response1['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response1['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response1['data']['events']);
$this->assertContains("databases.*.collections.*", $response1['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*", $response1['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}", $response1['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response1['data']['events']);
$this->assertContains("databases.*", $response1['data']['events']);
$this->assertNotEmpty($response1['data']['payload']);
$this->assertIsArray($response1['data']['payload']);
$this->assertArrayHasKey('$id', $response1['data']['payload']);
$this->assertEquals('Marvel Hero', $response1['data']['payload']['name']);
$this->assertArrayHasKey('$permissions', $response1['data']['payload']);
}
// client2 shouldn't receive any event and lead to timeout
try {
json_decode($client2->receive(), true);
$this->fail('Expected TimeoutException was not thrown.');
} catch (Exception $e) {
$this->assertInstanceOf(TimeoutException::class, $e);
}
// Perform bulk update(making it only accessible by user2)
$response = $this->client->call(Client::METHOD_PATCH, "/databases/{$databaseId}/collections/{$actorsId}/documents/", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'data' => [
'name' => 'Marvel Hero',
'$permissions' => [
Permission::read(Role::user($user2Id)),
Permission::update(Role::user($user2Id)),
Permission::delete(Role::user($user2Id)),
]
],
]);
// Receive and assert for client2
for ($i = 0; $i < 5; $i++) {
$response2 = json_decode($client2->receive(), true);
$this->assertArrayHasKey('type', $response2);
$this->assertArrayHasKey('data', $response2);
$this->assertEquals('event', $response2['type']);
$this->assertNotEmpty($response2['data']);
$this->assertArrayHasKey('timestamp', $response2['data']);
$this->assertCount(6, $response2['data']['channels']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response2['data']['payload']['$id']}.update", $response2['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*.update", $response2['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response2['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*.update", $response2['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response2['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*", $response2['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response2['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response2['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response2['data']['events']);
$this->assertContains("databases.*.collections.*", $response2['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*", $response2['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}", $response2['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response2['data']['events']);
$this->assertContains("databases.*", $response2['data']['events']);
$this->assertNotEmpty($response2['data']['payload']);
$this->assertIsArray($response2['data']['payload']);
$this->assertArrayHasKey('$id', $response2['data']['payload']);
$this->assertEquals('Marvel Hero', $response2['data']['payload']['name']);
$this->assertArrayHasKey('$permissions', $response2['data']['payload']);
}
// client1 shouldn't receive any event and lead to timeout
try {
json_decode($client1->receive(), true);
$this->fail('Expected TimeoutException was not thrown.');
} catch (Exception $e) {
$this->assertInstanceOf(TimeoutException::class, $e);
}
// Updating the permission for both the users
$response = $this->client->call(Client::METHOD_PATCH, "/databases/{$databaseId}/collections/{$actorsId}/documents/", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'data' => [
'name' => 'Marvel Hero',
'$permissions' => [
Permission::read(Role::users()),
Permission::update(Role::users()),
Permission::delete(Role::users()),
]
],
]);
// both user1 and user2 should receive the event
for ($i = 0; $i < 5; $i++) {
$response1 = json_decode($client1->receive(), true);
$this->assertArrayHasKey('type', $response1);
$this->assertArrayHasKey('data', $response1);
$this->assertEquals('event', $response1['type']);
$this->assertNotEmpty($response1['data']);
$this->assertArrayHasKey('timestamp', $response1['data']);
$this->assertCount(6, $response1['data']['channels']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response1['data']['payload']['$id']}.update", $response1['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*.update", $response1['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response1['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*.update", $response1['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response1['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*", $response1['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response1['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response1['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response1['data']['events']);
$this->assertContains("databases.*.collections.*", $response1['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*", $response1['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}", $response1['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response1['data']['events']);
$this->assertContains("databases.*", $response1['data']['events']);
$this->assertNotEmpty($response1['data']['payload']);
$this->assertIsArray($response1['data']['payload']);
$this->assertArrayHasKey('$id', $response1['data']['payload']);
$this->assertEquals('Marvel Hero', $response1['data']['payload']['name']);
$this->assertArrayHasKey('$permissions', $response1['data']['payload']);
$response2 = json_decode($client2->receive(), true);
$this->assertArrayHasKey('type', $response2);
$this->assertArrayHasKey('data', $response2);
$this->assertEquals('event', $response2['type']);
$this->assertNotEmpty($response2['data']);
$this->assertArrayHasKey('timestamp', $response2['data']);
$this->assertCount(6, $response2['data']['channels']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response2['data']['payload']['$id']}.update", $response2['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*.update", $response2['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response2['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*.update", $response2['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response2['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*", $response2['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response2['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response2['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response2['data']['events']);
$this->assertContains("databases.*.collections.*", $response2['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*", $response2['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}", $response2['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response2['data']['events']);
$this->assertContains("databases.*", $response2['data']['events']);
$this->assertNotEmpty($response2['data']['payload']);
$this->assertIsArray($response2['data']['payload']);
$this->assertArrayHasKey('$id', $response2['data']['payload']);
$this->assertEquals('Marvel Hero', $response2['data']['payload']['name']);
$this->assertArrayHasKey('$permissions', $response2['data']['payload']);
}
// Perform bulk delete
$response = $this->client->call(Client::METHOD_DELETE, "/databases/{$databaseId}/collections/{$actorsId}/documents", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(200, $response['headers']['status-code']);
// Receive and assert for client1
for ($i = 0; $i < 5; $i++) {
$response1 = json_decode($client1->receive(), true);
$this->assertArrayHasKey('type', $response1);
$this->assertArrayHasKey('data', $response1);
$this->assertEquals('event', $response1['type']);
$this->assertNotEmpty($response1['data']);
$this->assertArrayHasKey('timestamp', $response1['data']);
$this->assertCount(6, $response1['data']['channels']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response1['data']['payload']['$id']}.delete", $response1['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*.delete", $response1['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response1['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*.delete", $response1['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response1['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*", $response1['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response1['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response1['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response1['data']['events']);
$this->assertContains("databases.*.collections.*", $response1['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*", $response1['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}", $response1['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response1['data']['events']);
$this->assertContains("databases.*", $response1['data']['events']);
$this->assertNotEmpty($response1['data']['payload']);
$this->assertIsArray($response1['data']['payload']);
$this->assertArrayHasKey('$id', $response1['data']['payload']);
$this->assertArrayHasKey('name', $response1['data']['payload']);
$this->assertArrayHasKey('$permissions', $response1['data']['payload']);
$this->assertIsArray($response1['data']['payload']['$permissions']);
}
// Receive and assert for client2
for ($i = 0; $i < 5; $i++) {
$response2 = json_decode($client2->receive(), true);
$this->assertArrayHasKey('type', $response2);
$this->assertArrayHasKey('data', $response2);
$this->assertEquals('event', $response2['type']);
$this->assertNotEmpty($response2['data']);
$this->assertArrayHasKey('timestamp', $response2['data']);
$this->assertCount(6, $response2['data']['channels']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response2['data']['payload']['$id']}.delete", $response2['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*.delete", $response2['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response2['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*.delete", $response2['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response2['data']['events']);
$this->assertContains("databases.*.collections.*.documents.*", $response2['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response2['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response2['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response2['data']['events']);
$this->assertContains("databases.*.collections.*", $response2['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*", $response2['data']['events']);
$this->assertContains("databases.*.collections.{$actorsId}", $response2['data']['events']);
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response2['data']['events']);
$this->assertContains("databases.*", $response2['data']['events']);
$this->assertNotEmpty($response2['data']['payload']);
$this->assertIsArray($response2['data']['payload']);
$this->assertArrayHasKey('$id', $response2['data']['payload']);
$this->assertArrayHasKey('name', $response2['data']['payload']);
$this->assertArrayHasKey('$permissions', $response2['data']['payload']);
$this->assertIsArray($response2['data']['payload']['$permissions']);
}
$client1->close();
$client2->close();
}
public function testChannelDatabaseCollectionPermissions()
{
$user = $this->getUser();
@@ -0,0 +1,26 @@
$id,$createdAt,$updatedAt,$permissions,name,age
z1y2x3w4v5u6t7s8,2022-10-23T10:33:01+00:00,2023-03-15T12:00:41+00:00,"read(\"any\"),update(\"user:123\")",Diamond Mendez,56
r9q0p1o2n3m4l5k6,2021-08-11T21:05:13+00:00,2024-01-02T08:45:22+00:00,"read(\"any\"),update(\"user:456\")",Michael Huff,20
j7i8h9g0f1e2d3c4,2020-05-29T14:22:56+00:00,2022-11-30T18:19:33+00:00,"read(\"any\")",Alyssa Rodriguez,37
b5a6z7y8x9w0v1u2,2023-01-18T03:44:09+00:00,2023-09-07T23:50:17+00:00,"read(\"any\")",Barbara Smith,26
t3s4r5q6p7o8n9m0,2020-11-02T09:12:45+00:00,2021-07-21T15:30:55+00:00,"read(\"any\")",Evelyn Edwards,54
l1k2j3i4h5g6f7e8,2022-03-19T19:55:27+00:00,2024-05-14T06:28:11+00:00,"read(\"any\")",Tina Richardson,41
d9c0b1a2z3y4x5w6,2021-04-07T01:18:34+00:00,2023-06-25T11:47:04+00:00,"read(\"any\")",Joel Hernandez,49
v7u8t9s0r1q2p3o4,2023-08-22T16:40:18+00:00,2024-02-19T04:09:58+00:00,"read(\"any\")",Zachary Cooper,59
n5m6l7k8j9i0h1g2,2020-02-12T07:59:01+00:00,2022-09-08T13:21:49+00:00,"read(\"any\")",Brittany Spears,20
f3e4d5c6b7a8z9y0,2021-12-05T22:33:12+00:00,2023-11-11T02:55:37+00:00,"read(\"any\")",Holly White,47
x1w2v3u4t5s6r7q8,2022-07-14T05:01:50+00:00,2024-04-01T20:10:26+00:00,"read(\"any\")",Kimberly Barnes,27
p9o0n1m2l3k4j5i6,2020-09-28T11:27:36+00:00,2021-10-17T09:38:08+00:00,"read(\"any\")",Stephen Miller,53
h7g8f9e0d1c2b3a4,2023-04-04T08:15:59+00:00,2024-06-29T17:03:14+00:00,"read(\"any\")",Yvonne Newman,41
y5x6w7v8u9t0s1r2,2021-01-25T18:09:21+00:00,2022-08-16T22:44:51+00:00,"read(\"any\")",Carol Kane,38
q3p4o5n6m7l8k9j0,2022-06-09T12:53:47+00:00,2023-12-24T01:16:05+00:00,"read(\"any\")",Doris Foster,44
i1h2g3f4e5d6c7b8,2020-07-03T23:37:02+00:00,2021-05-09T05:52:43+00:00,"read(\"any\")",Joseph Stokes,28
a9z0y1x2w3v4u5t6,2023-10-10T02:20:15+00:00,2024-03-28T14:33:29+00:00,"read(\"any\")",Steve Williams,31
s7r8q9p0o1n2m3l4,2021-06-16T13:48:53+00:00,2022-04-22T07:07:19+00:00,"read(\"any\")",James Carey,29
k5j6i7h8g9f0e1d2,2022-12-27T20:06:38+00:00,2023-08-03T10:25:57+00:00,"read(\"any\")",Kathryn Henry,38
c3b4a5z6y7x8w9v0,2020-04-20T04:41:24+00:00,2021-02-13T19:14:06+00:00,"read(\"any\")",Christopher Landry,23
u1t2s3r4q5p6o7n8,2023-05-08T00:58:10+00:00,2024-07-05T03:36:48+00:00,"read(\"any\")",Jennifer Mcgee,62
m9l0k1j2i3h4g5f6,2021-09-01T06:11:42+00:00,2022-01-26T16:59:23+00:00,"read(\"any\")",Cathy Church,35
e7d8c9b0a1z2y3x4,2022-02-18T15:24:07+00:00,2023-04-12T00:40:31+00:00,"read(\"any\")",Jose Lopez,41
w5v6u7t8s9r0q1p2,2020-12-13T09:03:55+00:00,2021-11-06T11:23:16+00:00,"read(\"any\")",William Rose,30
o3n4m5l6k7j8i9h0,2021-12-13T09:03:55+00:00,2022-11-06T11:23:16+00:00,"read(\"any\")",Charles Hammer,61
Can't render this file because it contains an unexpected character in line 2 and column 77.