diff --git a/README.md b/README.md index 0288d5f4ba..d061e8c200 100644 --- a/README.md +++ b/README.md @@ -358,12 +358,14 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo ## Access Scopes -| Scope | Internal data | Custom data | Restricted by CLP, ACL | Key | -|----------------|---------------|-------------|------------------------|---------------------| -| Internal | r/w | r/w | no | `maintenanceKey` | -| Master | -/- | r/w | no | `masterKey` | -| ReadOnlyMaster | -/- | r/- | no | `readOnlyMasterKey` | -| Session | -/- | r/w | yes | `sessionToken` | +| Scope | Internal data | Read-only data (1) | Custom data | Restricted by CLP, ACL | Key | +|----------------|---------------|-------------------------------|-------------|------------------------|---------------------| +| Internal | r/w | r/w | r/w | no | `maintenanceKey` | +| Master | -/- | r/- | r/w | no | `masterKey` | +| ReadOnlyMaster | -/- | r/- | r/- | no | `readOnlyMasterKey` | +| Session | -/- | r/- | r/w | yes | `sessionToken` | + +(1) `Parse.Object.createdAt`, `Parse.Object.updatedAt`. ## Email Verification and Password Reset diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 376b57c90d..5430432163 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,58 @@ +# [6.4.0-alpha.8](https://github.com/parse-community/parse-server/compare/6.4.0-alpha.7...6.4.0-alpha.8) (2023-11-13) + + +### Features + +* Add compatibility for MongoDB Atlas Serverless and AWS Amazon DocumentDB with collation options `enableCollationCaseComparison`, `transformEmailToLowercase`, `transformUsernameToLowercase` ([#8805](https://github.com/parse-community/parse-server/issues/8805)) ([09fbeeb](https://github.com/parse-community/parse-server/commit/09fbeebba8870e7cf371fb84371a254c7b368620)) + +# [6.4.0-alpha.7](https://github.com/parse-community/parse-server/compare/6.4.0-alpha.6...6.4.0-alpha.7) (2023-10-25) + + +### Features + +* Add `$setOnInsert` operator to `Parse.Server.database.update` ([#8791](https://github.com/parse-community/parse-server/issues/8791)) ([f630a45](https://github.com/parse-community/parse-server/commit/f630a45aa5e87bc73a81fded061400c199b71a29)) + +# [6.4.0-alpha.6](https://github.com/parse-community/parse-server/compare/6.4.0-alpha.5...6.4.0-alpha.6) (2023-10-18) + + +### Bug Fixes + +* Security bump @babel/traverse from 7.20.5 to 7.23.2 ([#8777](https://github.com/parse-community/parse-server/issues/8777)) ([2d6b3d1](https://github.com/parse-community/parse-server/commit/2d6b3d18499179e99be116f25c0850d3f449509c)) + +# [6.4.0-alpha.5](https://github.com/parse-community/parse-server/compare/6.4.0-alpha.4...6.4.0-alpha.5) (2023-10-14) + + +### Bug Fixes + +* Context not passed to Cloud Code Trigger `beforeFind` when using `Parse.Query.include` ([#8765](https://github.com/parse-community/parse-server/issues/8765)) ([7d32d89](https://github.com/parse-community/parse-server/commit/7d32d8934f3ae7af7a7d8b9cc6a829c7d73973d3)) + +# [6.4.0-alpha.4](https://github.com/parse-community/parse-server/compare/6.4.0-alpha.3...6.4.0-alpha.4) (2023-09-29) + + +### Features + +* Allow setting `createdAt` and `updatedAt` during `Parse.Object` creation with maintenance key ([#8696](https://github.com/parse-community/parse-server/issues/8696)) ([77bbfb3](https://github.com/parse-community/parse-server/commit/77bbfb3f186f5651c33ba152f04cff95128eaf2d)) + +# [6.4.0-alpha.3](https://github.com/parse-community/parse-server/compare/6.4.0-alpha.2...6.4.0-alpha.3) (2023-09-23) + + +### Bug Fixes + +* Parse Server option `fileUpload.fileExtensions` fails to determine file extension if filename contains multiple dots ([#8754](https://github.com/parse-community/parse-server/issues/8754)) ([3d6d50e](https://github.com/parse-community/parse-server/commit/3d6d50e0afff18b95fb906914e2cebd3839b517a)) + +# [6.4.0-alpha.2](https://github.com/parse-community/parse-server/compare/6.4.0-alpha.1...6.4.0-alpha.2) (2023-09-22) + + +### Bug Fixes + +* Security upgrade graphql from 16.6.0 to 16.8.1 ([#8758](https://github.com/parse-community/parse-server/issues/8758)) ([71dfd8a](https://github.com/parse-community/parse-server/commit/71dfd8a7ece8c0dd1a66d03bb9420cfd39f4f9b1)) + +# [6.4.0-alpha.1](https://github.com/parse-community/parse-server/compare/6.3.0...6.4.0-alpha.1) (2023-09-20) + +### Features + +* Add context to Cloud Code Triggers `beforeLogin` and `afterLogin` ([#8724](https://github.com/parse-community/parse-server/issues/8724)) ([a9c34ef](https://github.com/parse-community/parse-server/commit/a9c34ef1e2c78a42fb8b5fa8d569b7677c74919d)) + # [6.3.0-alpha.9](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.8...6.3.0-alpha.9) (2023-09-13) diff --git a/package-lock.json b/package-lock.json index 1a390a4415..cfa865ab08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "6.4.0", + "version": "6.4.0-alpha.8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "6.4.0", + "version": "6.4.0-alpha.8", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -25,7 +25,7 @@ "express": "4.18.2", "express-rate-limit": "6.7.0", "follow-redirects": "1.15.2", - "graphql": "16.6.0", + "graphql": "16.8.1", "graphql-list-fields": "2.0.2", "graphql-relay": "0.10.0", "graphql-tag": "2.12.6", @@ -227,11 +227,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" @@ -325,12 +326,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.5.tgz", - "integrity": "sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dependencies": { - "@babel/types": "^7.20.5", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "engines": { @@ -376,9 +378,9 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "engines": { "node": ">=6.9.0" } @@ -396,23 +398,23 @@ } }, "node_modules/@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dependencies": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -520,28 +522,28 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } @@ -583,12 +585,12 @@ } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -596,9 +598,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", - "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "bin": { "parser": "bin/babel-parser.js" }, @@ -1797,31 +1799,31 @@ } }, "node_modules/@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.10", - "@babel/types": "^7.18.10" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.5.tgz", - "integrity": "sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==", - "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.5", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.5", - "@babel/types": "^7.20.5", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -1830,12 +1832,12 @@ } }, "node_modules/@babel/types": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz", - "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -8593,9 +8595,9 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" }, "node_modules/graphql": { - "version": "16.6.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz", - "integrity": "sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==", + "version": "16.8.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", + "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==", "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -12211,10 +12213,16 @@ "optional": true }, "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -16586,9 +16594,9 @@ } }, "node_modules/postcss": { - "version": "8.4.20", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz", - "integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "funding": [ { @@ -16598,10 +16606,14 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -20749,11 +20761,12 @@ } }, "@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "requires": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" } }, "@babel/compat-data": { @@ -20819,12 +20832,13 @@ } }, "@babel/generator": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.5.tgz", - "integrity": "sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "requires": { - "@babel/types": "^7.20.5", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "dependencies": { @@ -20860,9 +20874,9 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==" + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==" }, "@babel/helper-explode-assignable-expression": { "version": "7.18.6", @@ -20874,20 +20888,20 @@ } }, "@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "requires": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-member-expression-to-functions": { @@ -20968,22 +20982,22 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==" + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==" }, "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==" }, "@babel/helper-validator-option": { "version": "7.18.6", @@ -21013,19 +21027,19 @@ } }, "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", - "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==" + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==" }, "@babel/plugin-proposal-object-rest-spread": { "version": "7.10.0", @@ -21852,39 +21866,39 @@ } }, "@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.10", - "@babel/types": "^7.18.10" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" } }, "@babel/traverse": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.5.tgz", - "integrity": "sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==", - "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.5", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.5", - "@babel/types": "^7.20.5", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz", - "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "requires": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, @@ -27130,9 +27144,9 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" }, "graphql": { - "version": "16.6.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz", - "integrity": "sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==" + "version": "16.8.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", + "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==" }, "graphql-list-fields": { "version": "2.0.2", @@ -29958,9 +29972,9 @@ "optional": true }, "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", "dev": true }, "nanomatch": { @@ -33152,12 +33166,12 @@ "optional": true }, "postcss": { - "version": "8.4.20", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz", - "integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "requires": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } diff --git a/package.json b/package.json index 250ec29bb5..970d630cfa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "6.4.0", + "version": "6.4.0-alpha.8", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { @@ -34,7 +34,7 @@ "express": "4.18.2", "express-rate-limit": "6.7.0", "follow-redirects": "1.15.2", - "graphql": "16.6.0", + "graphql": "16.8.1", "graphql-list-fields": "2.0.2", "graphql-relay": "0.10.0", "graphql-tag": "2.12.6", diff --git a/spec/.eslintrc.json b/spec/.eslintrc.json index 8f8bcfeddc..ff45304cd5 100644 --- a/spec/.eslintrc.json +++ b/spec/.eslintrc.json @@ -15,6 +15,7 @@ "equal": true, "expectAsync": true, "notEqual": true, + "it_id": true, "it_only_db": true, "it_only_mongodb_version": true, "it_only_postgres_version": true, diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 14a64c4df4..a1c8b48bfd 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -2510,6 +2510,31 @@ describe('beforeFind hooks', () => { expect(res2.get('pointerFieldArray')[0].get('aField')).toBe('aFieldValue'); expect(spy).toHaveBeenCalledTimes(2); }); + + it('should have access to context in include query in beforeFind hook', async () => { + let beforeFindTestObjectCalled = false; + let beforeFindTestObject2Called = false; + const obj1 = new Parse.Object('TestObject'); + const obj2 = new Parse.Object('TestObject2'); + obj2.set('aField', 'aFieldValue'); + await obj2.save(); + obj1.set('pointerField', obj2); + await obj1.save(); + Parse.Cloud.beforeFind('TestObject', req => { + expect(req.context).toBeDefined(); + expect(req.context.a).toEqual('a'); + beforeFindTestObjectCalled = true; + }); + Parse.Cloud.beforeFind('TestObject2', req => { + expect(req.context).toBeDefined(); + expect(req.context.a).toEqual('a'); + beforeFindTestObject2Called = true; + }); + const query = new Parse.Query('TestObject'); + await query.include('pointerField').find({ context: { a: 'a' } }); + expect(beforeFindTestObjectCalled).toBeTrue(); + expect(beforeFindTestObject2Called).toBeTrue(); + }); }); describe('afterFind hooks', () => { @@ -3327,7 +3352,7 @@ describe('beforeLogin hook', () => { expect(req.headers).toBeDefined(); expect(req.ip).toBeDefined(); expect(req.installationId).toBeDefined(); - expect(req.context).toBeUndefined(); + expect(req.context).toBeDefined(); }); await Parse.User.signUp('tupac', 'shakur'); @@ -3444,7 +3469,7 @@ describe('afterLogin hook', () => { expect(req.headers).toBeDefined(); expect(req.ip).toBeDefined(); expect(req.installationId).toBeDefined(); - expect(req.context).toBeUndefined(); + expect(req.context).toBeDefined(); }); await Parse.User.signUp('testuser', 'p@ssword'); diff --git a/spec/DatabaseController.spec.js b/spec/DatabaseController.spec.js index 98103ce6e4..e1b50a5a52 100644 --- a/spec/DatabaseController.spec.js +++ b/spec/DatabaseController.spec.js @@ -1,3 +1,4 @@ +const Config = require('../lib/Config'); const DatabaseController = require('../lib/Controllers/DatabaseController.js'); const validateQuery = DatabaseController._validateQuery; @@ -361,6 +362,259 @@ describe('DatabaseController', function () { done(); }); }); + + describe('enableCollationCaseComparison', () => { + const dummyStorageAdapter = { + find: () => Promise.resolve([]), + watch: () => Promise.resolve(), + getAllClasses: () => Promise.resolve([]), + }; + + beforeEach(() => { + Config.get(Parse.applicationId).schemaCache.clear(); + }); + + it('should force caseInsensitive to false with enableCollationCaseComparison option', async () => { + const databaseController = new DatabaseController(dummyStorageAdapter, { + enableCollationCaseComparison: true, + }); + const spy = spyOn(dummyStorageAdapter, 'find'); + spy.and.callThrough(); + await databaseController.find('SomeClass', {}, { caseInsensitive: true }); + expect(spy.calls.all()[0].args[3].caseInsensitive).toEqual(false); + }); + + it('should support caseInsensitive without enableCollationCaseComparison option', async () => { + const databaseController = new DatabaseController(dummyStorageAdapter, {}); + const spy = spyOn(dummyStorageAdapter, 'find'); + spy.and.callThrough(); + await databaseController.find('_User', {}, { caseInsensitive: true }); + expect(spy.calls.all()[0].args[3].caseInsensitive).toEqual(true); + }); + + it_only_db('mongo')( + 'should create insensitive indexes without enableCollationCaseComparison', + async () => { + await reconfigureServer({ + databaseURI: 'mongodb://localhost:27017/enableCollationCaseComparisonFalse', + databaseAdapter: undefined, + }); + const user = new Parse.User(); + await user.save({ + username: 'example', + password: 'password', + email: 'example@example.com', + }); + const schemas = await Parse.Schema.all(); + const UserSchema = schemas.find(({ className }) => className === '_User'); + expect(UserSchema.indexes).toEqual({ + _id_: { _id: 1 }, + username_1: { username: 1 }, + case_insensitive_username: { username: 1 }, + case_insensitive_email: { email: 1 }, + email_1: { email: 1 }, + }); + } + ); + + it_only_db('mongo')( + 'should not create insensitive indexes with enableCollationCaseComparison', + async () => { + await reconfigureServer({ + enableCollationCaseComparison: true, + databaseURI: 'mongodb://localhost:27017/enableCollationCaseComparisonTrue', + databaseAdapter: undefined, + }); + const user = new Parse.User(); + await user.save({ + username: 'example', + password: 'password', + email: 'example@example.com', + }); + const schemas = await Parse.Schema.all(); + const UserSchema = schemas.find(({ className }) => className === '_User'); + expect(UserSchema.indexes).toEqual({ + _id_: { _id: 1 }, + username_1: { username: 1 }, + email_1: { email: 1 }, + }); + } + ); + }); + + describe('convertEmailToLowercase', () => { + const dummyStorageAdapter = { + createObject: () => Promise.resolve({ ops: [{}] }), + findOneAndUpdate: () => Promise.resolve({}), + watch: () => Promise.resolve(), + getAllClasses: () => + Promise.resolve([ + { + className: '_User', + fields: { email: 'String' }, + indexes: {}, + classLevelPermissions: { protectedFields: {} }, + }, + ]), + }; + const dates = { + createdAt: { iso: undefined, __type: 'Date' }, + updatedAt: { iso: undefined, __type: 'Date' }, + }; + + it('should not transform email to lower case without convertEmailToLowercase option on create', async () => { + const databaseController = new DatabaseController(dummyStorageAdapter, {}); + const spy = spyOn(dummyStorageAdapter, 'createObject'); + spy.and.callThrough(); + await databaseController.create('_User', { + email: 'EXAMPLE@EXAMPLE.COM', + }); + expect(spy.calls.all()[0].args[2]).toEqual({ + email: 'EXAMPLE@EXAMPLE.COM', + ...dates, + }); + }); + + it('should transform email to lower case with convertEmailToLowercase option on create', async () => { + const databaseController = new DatabaseController(dummyStorageAdapter, { + convertEmailToLowercase: true, + }); + const spy = spyOn(dummyStorageAdapter, 'createObject'); + spy.and.callThrough(); + await databaseController.create('_User', { + email: 'EXAMPLE@EXAMPLE.COM', + }); + expect(spy.calls.all()[0].args[2]).toEqual({ + email: 'example@example.com', + ...dates, + }); + }); + + it('should not transform email to lower case without convertEmailToLowercase option on update', async () => { + const databaseController = new DatabaseController(dummyStorageAdapter, {}); + const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate'); + spy.and.callThrough(); + await databaseController.update('_User', { id: 'example' }, { email: 'EXAMPLE@EXAMPLE.COM' }); + expect(spy.calls.all()[0].args[3]).toEqual({ + email: 'EXAMPLE@EXAMPLE.COM', + }); + }); + + it('should transform email to lower case with convertEmailToLowercase option on update', async () => { + const databaseController = new DatabaseController(dummyStorageAdapter, { + convertEmailToLowercase: true, + }); + const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate'); + spy.and.callThrough(); + await databaseController.update('_User', { id: 'example' }, { email: 'EXAMPLE@EXAMPLE.COM' }); + expect(spy.calls.all()[0].args[3]).toEqual({ + email: 'example@example.com', + }); + }); + + it('should not find a case insensitive user by email with convertEmailToLowercase', async () => { + await reconfigureServer({ convertEmailToLowercase: true }); + const user = new Parse.User(); + await user.save({ username: 'EXAMPLE', email: 'EXAMPLE@EXAMPLE.COM', password: 'password' }); + + const query = new Parse.Query(Parse.User); + query.equalTo('email', 'EXAMPLE@EXAMPLE.COM'); + const result = await query.find({ useMasterKey: true }); + expect(result.length).toEqual(0); + + const query2 = new Parse.Query(Parse.User); + query2.equalTo('email', 'example@example.com'); + const result2 = await query2.find({ useMasterKey: true }); + expect(result2.length).toEqual(1); + }); + }); + + describe('convertUsernameToLowercase', () => { + const dummyStorageAdapter = { + createObject: () => Promise.resolve({ ops: [{}] }), + findOneAndUpdate: () => Promise.resolve({}), + watch: () => Promise.resolve(), + getAllClasses: () => + Promise.resolve([ + { + className: '_User', + fields: { username: 'String' }, + indexes: {}, + classLevelPermissions: { protectedFields: {} }, + }, + ]), + }; + const dates = { + createdAt: { iso: undefined, __type: 'Date' }, + updatedAt: { iso: undefined, __type: 'Date' }, + }; + + it('should not transform username to lower case without convertUsernameToLowercase option on create', async () => { + const databaseController = new DatabaseController(dummyStorageAdapter, {}); + const spy = spyOn(dummyStorageAdapter, 'createObject'); + spy.and.callThrough(); + await databaseController.create('_User', { + username: 'EXAMPLE', + }); + expect(spy.calls.all()[0].args[2]).toEqual({ + username: 'EXAMPLE', + ...dates, + }); + }); + + it('should transform username to lower case with convertUsernameToLowercase option on create', async () => { + const databaseController = new DatabaseController(dummyStorageAdapter, { + convertUsernameToLowercase: true, + }); + const spy = spyOn(dummyStorageAdapter, 'createObject'); + spy.and.callThrough(); + await databaseController.create('_User', { + username: 'EXAMPLE', + }); + expect(spy.calls.all()[0].args[2]).toEqual({ + username: 'example', + ...dates, + }); + }); + + it('should not transform username to lower case without convertUsernameToLowercase option on update', async () => { + const databaseController = new DatabaseController(dummyStorageAdapter, {}); + const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate'); + spy.and.callThrough(); + await databaseController.update('_User', { id: 'example' }, { username: 'EXAMPLE' }); + expect(spy.calls.all()[0].args[3]).toEqual({ + username: 'EXAMPLE', + }); + }); + + it('should transform username to lower case with convertUsernameToLowercase option on update', async () => { + const databaseController = new DatabaseController(dummyStorageAdapter, { + convertUsernameToLowercase: true, + }); + const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate'); + spy.and.callThrough(); + await databaseController.update('_User', { id: 'example' }, { username: 'EXAMPLE' }); + expect(spy.calls.all()[0].args[3]).toEqual({ + username: 'example', + }); + }); + + it('should not find a case insensitive user by username with convertUsernameToLowercase', async () => { + await reconfigureServer({ convertUsernameToLowercase: true }); + const user = new Parse.User(); + await user.save({ username: 'EXAMPLE', password: 'password' }); + + const query = new Parse.Query(Parse.User); + query.equalTo('username', 'EXAMPLE'); + const result = await query.find({ useMasterKey: true }); + expect(result.length).toEqual(0); + + const query2 = new Parse.Query(Parse.User); + query2.equalTo('username', 'example'); + const result2 = await query2.find({ useMasterKey: true }); + expect(result2.length).toEqual(1); + }); + }); }); function buildCLP(pointerNames) { diff --git a/spec/MongoStorageAdapter.spec.js b/spec/MongoStorageAdapter.spec.js index 1b5cc0c5e9..25a6ae5639 100644 --- a/spec/MongoStorageAdapter.spec.js +++ b/spec/MongoStorageAdapter.spec.js @@ -254,6 +254,61 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { expect(obj.get('foo').test.date[0] instanceof Date).toBeTrue(); }); + it('upserts with $setOnInsert', async () => { + const uuid = require('uuid'); + const uuid1 = uuid.v4(); + const uuid2 = uuid.v4(); + const schema = { + className: 'MyClass', + fields: { + x: { type: 'Number' }, + count: { type: 'Number' }, + }, + classLevelPermissions: {}, + }; + + const myClassSchema = new Parse.Schema(schema.className); + myClassSchema.setCLP(schema.classLevelPermissions); + await myClassSchema.save(); + + const query = { + x: 1, + }; + const update = { + objectId: { + __op: 'SetOnInsert', + amount: uuid1, + }, + count: { + __op: 'Increment', + amount: 1, + }, + }; + await Parse.Server.database.update( + 'MyClass', + query, + update, + { upsert: true }, + ); + update.objectId.amount = uuid2; + await Parse.Server.database.update( + 'MyClass', + query, + update, + { upsert: true }, + ); + + const res = await Parse.Server.database.find( + schema.className, + {}, + {}, + ); + expect(res.length).toBe(1); + expect(res[0].objectId).toBe(uuid1); + expect(res[0].count).toBe(2); + expect(res[0].x).toBe(1); + }); + it('handles updating a single object with array, object date', done => { const adapter = new MongoStorageAdapter({ uri: databaseURI }); diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index 5f5ab43c54..e36929636b 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -1364,6 +1364,74 @@ describe('Parse.File testing', () => { ); }); + it('works with a period in the file name', async () => { + await reconfigureServer({ + fileUpload: { + enableForPublic: true, + fileExtensions: ['^[^hH][^tT][^mM][^lL]?$'], + }, + }); + const headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + + const values = ['file.png.html', 'file.txt.png.html', 'file.png.txt.html']; + + for (const value of values) { + await expectAsync( + request({ + method: 'POST', + headers: headers, + url: `http://localhost:8378/1/files/${value}`, + body: '\n', + }).catch(e => { + throw new Error(e.data.error); + }) + ).toBeRejectedWith( + new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`) + ); + } + }); + + it('works to stop invalid filenames', async () => { + await reconfigureServer({ + fileUpload: { + enableForPublic: true, + fileExtensions: ['^[^hH][^tT][^mM][^lL]?$'], + }, + }); + const headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + + const values = [ + '!invalid.png', + '.png', + '.html', + ' .html', + '.png.html', + '~invalid.png', + '-invalid.png', + ]; + + for (const value of values) { + await expectAsync( + request({ + method: 'POST', + headers: headers, + url: `http://localhost:8378/1/files/${value}`, + body: '\n', + }).catch(e => { + throw new Error(e.data.error); + }) + ).toBeRejectedWith( + new Parse.Error(Parse.Error.INVALID_FILE_NAME, `Filename contains invalid characters.`) + ); + } + }); + it('allows file without extension', async () => { await reconfigureServer({ fileUpload: { diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 4d3beaf349..99439e3803 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -107,6 +107,36 @@ describe('Parse.User testing', () => { } }); + it('user login with context', async () => { + let hit = 0; + const context = { foo: 'bar' }; + Parse.Cloud.beforeLogin(req => { + expect(req.context).toEqual(context); + hit++; + }); + Parse.Cloud.afterLogin(req => { + expect(req.context).toEqual(context); + hit++; + }); + await Parse.User.signUp('asdf', 'zxcv'); + await request({ + method: 'POST', + url: 'http://localhost:8378/1/login', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Cloud-Context': JSON.stringify(context), + 'Content-Type': 'application/json', + }, + body: { + _method: 'GET', + username: 'asdf', + password: 'zxcv', + }, + }); + expect(hit).toBe(2); + }); + it('user login with non-string username with REST API', async done => { await Parse.User.signUp('asdf', 'zxcv'); request({ diff --git a/spec/helper.js b/spec/helper.js index 445de26509..d393ef1d17 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -103,6 +103,7 @@ const defaultConfiguration = { restAPIKey: 'rest', webhookKey: 'hook', masterKey: 'test', + maintenanceKey: 'testing', readOnlyMasterKey: 'read-only-test', fileKey: 'test', directAccess: true, @@ -250,8 +251,8 @@ afterEach(function (done) { }) .then(() => Parse.User.logOut()) .then( - () => {}, - () => {} + () => { }, + () => { } ) // swallow errors .then(() => { // Connection close events are not immediate on node 10+... wait a bit @@ -427,6 +428,29 @@ global.it_exclude_dbs = excluded => { } }; +let testExclusionList = []; +try { + // Fetch test exclusion list + testExclusionList = require('./testExclusionList.json'); + console.log(`Using test exclusion list with ${testExclusionList.length} entries`); +} catch(error) { + if(error.code !== 'MODULE_NOT_FOUND') { + throw error; + } +} + +// Disable test if its UUID is found in testExclusionList +global.it_id = (id, func) => { + if (testExclusionList.includes(id)) { + return xit; + } else { + if(func === undefined) + return it; + else + return func; + } +}; + global.it_only_db = db => { if ( process.env.PARSE_SERVER_TEST_DB === db || diff --git a/spec/rest.spec.js b/spec/rest.spec.js index 61a5c728e4..dadba596c4 100644 --- a/spec/rest.spec.js +++ b/spec/rest.spec.js @@ -136,6 +136,119 @@ describe('rest create', () => { }); }); + describe('with maintenance key', () => { + let req; + + async function getObject(id) { + const res = await request({ + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }, + method: 'GET', + url: `http://localhost:8378/1/classes/TestObject/${id}` + }); + + return res.data; + } + + beforeEach(() => { + req = { + headers: { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Maintenance-Key': 'testing' + }, + method: 'POST', + url: 'http://localhost:8378/1/classes/TestObject' + }; + }); + + it('allows createdAt', async () => { + const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' }; + req.body = { createdAt }; + + const res = await request(req); + expect(res.data.createdAt).toEqual(createdAt.iso); + }); + + it('allows createdAt and updatedAt', async () => { + const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' }; + const updatedAt = { __type: 'Date', iso: '2019-02-01T00:00:00.000Z' }; + req.body = { createdAt, updatedAt }; + + const res = await request(req); + + const obj = await getObject(res.data.objectId); + expect(obj.createdAt).toEqual(createdAt.iso); + expect(obj.updatedAt).toEqual(updatedAt.iso); + }); + + it('allows createdAt, updatedAt, and additional field', async () => { + const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' }; + const updatedAt = { __type: 'Date', iso: '2019-02-01T00:00:00.000Z' }; + req.body = { createdAt, updatedAt, testing: 123 }; + + const res = await request(req); + + const obj = await getObject(res.data.objectId); + expect(obj.createdAt).toEqual(createdAt.iso); + expect(obj.updatedAt).toEqual(updatedAt.iso); + expect(obj.testing).toEqual(123); + }); + + it('cannot set updatedAt dated before createdAt', async () => { + const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' }; + const updatedAt = { __type: 'Date', iso: '2018-12-01T00:00:00.000Z' }; + req.body = { createdAt, updatedAt }; + + try { + await request(req); + fail(); + } + catch (err) { + expect(err.data.code).toEqual(Parse.Error.VALIDATION_ERROR); + } + }); + + it('cannot set updatedAt without createdAt', async () => { + const updatedAt = { __type: 'Date', iso: '2018-12-01T00:00:00.000Z' }; + req.body = { updatedAt }; + + const res = await request(req); + + const obj = await getObject(res.data.objectId); + expect(obj.updatedAt).not.toEqual(updatedAt.iso); + }); + + it('handles bad types for createdAt and updatedAt', async () => { + const createdAt = 12345; + const updatedAt = true; + req.body = { createdAt, updatedAt }; + + try { + await request(req); + fail(); + } + catch (err) { + expect(err.data.code).toEqual(Parse.Error.INCORRECT_TYPE); + } + }); + + it('cannot set createdAt or updatedAt without maintenance key', async () => { + const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' }; + const updatedAt = { __type: 'Date', iso: '2019-02-01T00:00:00.000Z' }; + req.body = { createdAt, updatedAt }; + delete req.headers['X-Parse-Maintenance-Key']; + + const res = await request(req); + + expect(res.data.createdAt).not.toEqual(createdAt.iso); + expect(res.data.updatedAt).not.toEqual(updatedAt.iso); + }); + }); + it('handles array, object, date', done => { const now = new Date(); const obj = { diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 6f6811cec3..7f66fb32fb 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -986,6 +986,13 @@ function transformUpdateOperator({ __op, amount, objects }, flatten) { return { __op: '$inc', arg: amount }; } + case 'SetOnInsert': + if (flatten) { + return amount; + } else { + return { __op: '$setOnInsert', arg: amount }; + } + case 'Add': case 'AddUnique': if (!(objects instanceof Array)) { diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index f9c782db87..5975e94053 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -279,6 +279,9 @@ const flattenUpdateOperatorsForCreate = object => { } object[key] = object[key].amount; break; + case 'SetOnInsert': + object[key] = object[key].amount; + break; case 'Add': if (!(object[key].objects instanceof Array)) { throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to add must be an array'); @@ -365,6 +368,22 @@ const relationSchema = { fields: { relatedId: { type: 'String' }, owningId: { type: 'String' } }, }; +const convertEmailToLowercase = (object, className, options) => { + if (className === '_User' && options.convertEmailToLowercase) { + if (typeof object['email'] === 'string') { + object['email'] = object['email'].toLowerCase(); + } + } +}; + +const convertUsernameToLowercase = (object, className, options) => { + if (className === '_User' && options.convertUsernameToLowercase) { + if (typeof object['username'] === 'string') { + object['username'] = object['username'].toLowerCase(); + } + } +}; + class DatabaseController { adapter: StorageAdapter; schemaCache: any; @@ -570,6 +589,8 @@ class DatabaseController { } } update = transformObjectACL(update); + convertEmailToLowercase(update, className, this.options); + convertUsernameToLowercase(update, className, this.options); transformAuthData(className, update, schema); if (validateOnly) { return this.adapter.find(className, schema, query, {}).then(result => { @@ -819,6 +840,8 @@ class DatabaseController { const originalObject = object; object = transformObjectACL(object); + convertEmailToLowercase(object, className, this.options); + convertUsernameToLowercase(object, className, this.options); object.createdAt = { iso: object.createdAt, __type: 'Date' }; object.updatedAt = { iso: object.updatedAt, __type: 'Date' }; @@ -1212,7 +1235,7 @@ class DatabaseController { keys, readPreference, hint, - caseInsensitive, + caseInsensitive: this.options.enableCollationCaseComparison ? false : caseInsensitive, explain, }; Object.keys(sort).forEach(fieldName => { @@ -1716,25 +1739,27 @@ class DatabaseController { throw error; }); - await this.adapter - .ensureIndex('_User', requiredUserFields, ['username'], 'case_insensitive_username', true) - .catch(error => { - logger.warn('Unable to create case insensitive username index: ', error); - throw error; - }); + if (!this.options.enableCollationCaseComparison) { + await this.adapter + .ensureIndex('_User', requiredUserFields, ['username'], 'case_insensitive_username', true) + .catch(error => { + logger.warn('Unable to create case insensitive username index: ', error); + throw error; + }); + + await this.adapter + .ensureIndex('_User', requiredUserFields, ['email'], 'case_insensitive_email', true) + .catch(error => { + logger.warn('Unable to create case insensitive email index: ', error); + throw error; + }); + } await this.adapter.ensureUniqueness('_User', requiredUserFields, ['email']).catch(error => { logger.warn('Unable to ensure uniqueness for user email addresses: ', error); throw error; }); - await this.adapter - .ensureIndex('_User', requiredUserFields, ['email'], 'case_insensitive_email', true) - .catch(error => { - logger.warn('Unable to create case insensitive email index: ', error); - throw error; - }); - await this.adapter.ensureUniqueness('_Role', requiredRoleFields, ['name']).catch(error => { logger.warn('Unable to ensure uniqueness for role name: ', error); throw error; @@ -1817,7 +1842,7 @@ class DatabaseController { keyUpdate && typeof keyUpdate === 'object' && keyUpdate.__op && - ['Add', 'AddUnique', 'Remove', 'Increment'].indexOf(keyUpdate.__op) > -1 + ['Add', 'AddUnique', 'Remove', 'Increment', 'SetOnInsert'].indexOf(keyUpdate.__op) > -1 ) { // only valid ops that produce an actionable result // the op may have happened on a keypath diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 7a1e56bad0..c37e6adfc3 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -139,6 +139,20 @@ module.exports.ParseServerOptions = { help: 'A collection prefix for the classes', default: '', }, + convertEmailToLowercase: { + env: 'PARSE_SERVER_CONVERT_EMAIL_TO_LOWERCASE', + help: + 'Optional. If set to `true`, the `email` property of a user is automatically converted to lowercase before being stored in the database. Consequently, queries must match the case as stored in the database, which would be lowercase in this scenario. If `false`, the `email` property is stored as set, without any case modifications. Default is `false`.', + action: parsers.booleanParser, + default: false, + }, + convertUsernameToLowercase: { + env: 'PARSE_SERVER_CONVERT_USERNAME_TO_LOWERCASE', + help: + 'Optional. If set to `true`, the `username` property of a user is automatically converted to lowercase before being stored in the database. Consequently, queries must match the case as stored in the database, which would be lowercase in this scenario. If `false`, the `username` property is stored as set, without any case modifications. Default is `false`.', + action: parsers.booleanParser, + default: false, + }, customPages: { env: 'PARSE_SERVER_CUSTOM_PAGES', help: 'custom pages for password validation and reset', @@ -203,6 +217,13 @@ module.exports.ParseServerOptions = { action: parsers.booleanParser, default: true, }, + enableCollationCaseComparison: { + env: 'PARSE_SERVER_ENABLE_COLLATION_CASE_COMPARISON', + help: + 'Optional. If set to `true`, the collation rule of case comparison for queries and indexes is enabled. Enable this option to run Parse Server with MongoDB Atlas Serverless or AWS Amazon DocumentDB. If `false`, the collation rule of case comparison is disabled. Default is `false`.', + action: parsers.booleanParser, + default: false, + }, enableExpressErrorHandler: { env: 'PARSE_SERVER_ENABLE_EXPRESS_ERROR_HANDLER', help: 'Enables the default express error handler for all errors', @@ -317,7 +338,7 @@ module.exports.ParseServerOptions = { maintenanceKey: { env: 'PARSE_SERVER_MAINTENANCE_KEY', help: - '(Optional) The maintenance key is used for modifying internal fields of Parse Server.

\u26A0\uFE0F This key is not intended to be used as part of a regular operation of Parse Server. This key is intended to conduct out-of-band changes such as one-time migrations or data correction tasks. Internal fields are not officially documented and may change at any time without publication in release changelogs. We strongly advice not to rely on internal fields as part of your regular operation and to investigate the implications of any planned changes *directly in the source code* of your current version of Parse Server.', + '(Optional) The maintenance key is used for modifying internal and read-only fields of Parse Server.

\u26A0\uFE0F This key is not intended to be used as part of a regular operation of Parse Server. This key is intended to conduct out-of-band changes such as one-time migrations or data correction tasks. Internal fields are not officially documented and may change at any time without publication in release changelogs. We strongly advice not to rely on internal fields as part of your regular operation and to investigate the implications of any planned changes *directly in the source code* of your current version of Parse Server.', required: true, }, maintenanceKeyIps: { diff --git a/src/Options/docs.js b/src/Options/docs.js index 09e6f5b3b4..cef358ad63 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -28,6 +28,8 @@ * @property {String} cloud Full path to your cloud code main.js * @property {Number|Boolean} cluster Run with cluster, optionally set the number of processes default to os.cpus().length * @property {String} collectionPrefix A collection prefix for the classes + * @property {Boolean} convertEmailToLowercase Optional. If set to `true`, the `email` property of a user is automatically converted to lowercase before being stored in the database. Consequently, queries must match the case as stored in the database, which would be lowercase in this scenario. If `false`, the `email` property is stored as set, without any case modifications. Default is `false`. + * @property {Boolean} convertUsernameToLowercase Optional. If set to `true`, the `username` property of a user is automatically converted to lowercase before being stored in the database. Consequently, queries must match the case as stored in the database, which would be lowercase in this scenario. If `false`, the `username` property is stored as set, without any case modifications. Default is `false`. * @property {CustomPagesOptions} customPages custom pages for password validation and reset * @property {Adapter} databaseAdapter Adapter module for the database; any options that are not explicitly described here are passed directly to the database client. * @property {DatabaseOptions} databaseOptions Options to pass to the database client @@ -39,6 +41,7 @@ * @property {Boolean} emailVerifyTokenReuseIfValid Set to `true` if a email verification token should be reused in case another token is requested but there is a token that is still valid, i.e. has not expired. This avoids the often observed issue that a user requests multiple emails and does not know which link contains a valid token because each newly generated token would invalidate the previous token.

Default is `false`.
Requires option `verifyUserEmails: true`. * @property {Number} emailVerifyTokenValidityDuration Set the validity duration of the email verification token in seconds after which the token expires. The token is used in the link that is set in the email. After the token expires, the link becomes invalid and a new link has to be sent. If the option is not set or set to `undefined`, then the token never expires.

For example, to expire the token after 2 hours, set a value of 7200 seconds (= 60 seconds * 60 minutes * 2 hours).

Default is `undefined`.
Requires option `verifyUserEmails: true`. * @property {Boolean} enableAnonymousUsers Enable (or disable) anonymous users, defaults to true + * @property {Boolean} enableCollationCaseComparison Optional. If set to `true`, the collation rule of case comparison for queries and indexes is enabled. Enable this option to run Parse Server with MongoDB Atlas Serverless or AWS Amazon DocumentDB. If `false`, the collation rule of case comparison is disabled. Default is `false`. * @property {Boolean} enableExpressErrorHandler Enables the default express error handler for all errors * @property {Boolean} encodeParseObjectInCloudFunction If set to `true`, a `Parse.Object` that is in the payload when calling a Cloud Function will be converted to an instance of `Parse.Object`. If `false`, the object will not be converted and instead be a plain JavaScript object, which contains the raw data of a `Parse.Object` but is not an actual instance of `Parse.Object`. Default is `false`.

ℹ️ The expected behavior would be that the object is converted to an instance of `Parse.Object`, so you would normally set this option to `true`. The default is `false` because this is a temporary option that has been introduced to avoid a breaking change when fixing a bug where JavaScript objects are not converted to actual instances of `Parse.Object`. * @property {String} encryptionKey Key for encrypting your files @@ -60,7 +63,7 @@ * @property {String} logLevel Sets the level for logs * @property {LogLevels} logLevels (Optional) Overrides the log levels used internally by Parse Server to log events. * @property {String} logsFolder Folder for the logs (defaults to './logs'); set to null to disable file based logging - * @property {String} maintenanceKey (Optional) The maintenance key is used for modifying internal fields of Parse Server.

⚠️ This key is not intended to be used as part of a regular operation of Parse Server. This key is intended to conduct out-of-band changes such as one-time migrations or data correction tasks. Internal fields are not officially documented and may change at any time without publication in release changelogs. We strongly advice not to rely on internal fields as part of your regular operation and to investigate the implications of any planned changes *directly in the source code* of your current version of Parse Server. + * @property {String} maintenanceKey (Optional) The maintenance key is used for modifying internal and read-only fields of Parse Server.

⚠️ This key is not intended to be used as part of a regular operation of Parse Server. This key is intended to conduct out-of-band changes such as one-time migrations or data correction tasks. Internal fields are not officially documented and may change at any time without publication in release changelogs. We strongly advice not to rely on internal fields as part of your regular operation and to investigate the implications of any planned changes *directly in the source code* of your current version of Parse Server. * @property {String[]} maintenanceKeyIps (Optional) Restricts the use of maintenance key permissions to a list of IP addresses.

This option accepts a list of single IP addresses, for example:
`['10.0.0.1', '10.0.0.2']`

You can also use CIDR notation to specify an IP address range, for example:
`['10.0.1.0/24']`

Special cases:
- Setting an empty array `[]` means that `maintenanceKey` cannot be used even in Parse Server Cloud Code.
- Setting `['0.0.0.0/0']` means disabling the filter and the maintenance key can be used from any IP address.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the maintenance key. * @property {String} masterKey Your Parse Master Key * @property {String[]} masterKeyIps (Optional) Restricts the use of master key permissions to a list of IP addresses.

This option accepts a list of single IP addresses, for example:
`['10.0.0.1', '10.0.0.2']`

You can also use CIDR notation to specify an IP address range, for example:
`['10.0.1.0/24']`

Special cases:
- Setting an empty array `[]` means that `masterKey` cannot be used even in Parse Server Cloud Code.
- Setting `['0.0.0.0/0']` means disabling the filter and the master key can be used from any IP address.

To connect Parse Dashboard from a different server requires to add the IP address of the server that hosts Parse Dashboard because Parse Dashboard uses the master key.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the master key. diff --git a/src/Options/index.js b/src/Options/index.js index d501b996dd..9af83ecd82 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -47,7 +47,7 @@ export interface ParseServerOptions { appId: string; /* Your Parse Master Key */ masterKey: string; - /* (Optional) The maintenance key is used for modifying internal fields of Parse Server.

⚠️ This key is not intended to be used as part of a regular operation of Parse Server. This key is intended to conduct out-of-band changes such as one-time migrations or data correction tasks. Internal fields are not officially documented and may change at any time without publication in release changelogs. We strongly advice not to rely on internal fields as part of your regular operation and to investigate the implications of any planned changes *directly in the source code* of your current version of Parse Server. */ + /* (Optional) The maintenance key is used for modifying internal and read-only fields of Parse Server.

⚠️ This key is not intended to be used as part of a regular operation of Parse Server. This key is intended to conduct out-of-band changes such as one-time migrations or data correction tasks. Internal fields are not officially documented and may change at any time without publication in release changelogs. We strongly advice not to rely on internal fields as part of your regular operation and to investigate the implications of any planned changes *directly in the source code* of your current version of Parse Server. */ maintenanceKey: string; /* URL to your parse server with http:// or https://. :ENV: PARSE_SERVER_URL */ @@ -103,6 +103,15 @@ export interface ParseServerOptions { databaseOptions: ?DatabaseOptions; /* Adapter module for the database; any options that are not explicitly described here are passed directly to the database client. */ databaseAdapter: ?Adapter; + /* Optional. If set to `true`, the collation rule of case comparison for queries and indexes is enabled. Enable this option to run Parse Server with MongoDB Atlas Serverless or AWS Amazon DocumentDB. If `false`, the collation rule of case comparison is disabled. Default is `false`. + :DEFAULT: false */ + enableCollationCaseComparison: ?boolean; + /* Optional. If set to `true`, the `email` property of a user is automatically converted to lowercase before being stored in the database. Consequently, queries must match the case as stored in the database, which would be lowercase in this scenario. If `false`, the `email` property is stored as set, without any case modifications. Default is `false`. + :DEFAULT: false */ + convertEmailToLowercase: ?boolean; + /* Optional. If set to `true`, the `username` property of a user is automatically converted to lowercase before being stored in the database. Consequently, queries must match the case as stored in the database, which would be lowercase in this scenario. If `false`, the `username` property is stored as set, without any case modifications. Default is `false`. + :DEFAULT: false */ + convertUsernameToLowercase: ?boolean; /* Full path to your cloud code main.js */ cloud: ?string; /* A collection prefix for the classes diff --git a/src/RestQuery.js b/src/RestQuery.js index 96a52ec17a..5af678bb96 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -478,6 +478,7 @@ _UnsafeRestQuery.prototype.replaceInQuery = async function () { className: inQueryValue.className, restWhere: inQueryValue.where, restOptions: additionalOptions, + context: this.context, }); return subquery.execute().then(response => { transformInQuery(inQueryObject, subquery.className, response.results); @@ -537,6 +538,7 @@ _UnsafeRestQuery.prototype.replaceNotInQuery = async function () { className: notInQueryValue.className, restWhere: notInQueryValue.where, restOptions: additionalOptions, + context: this.context, }); return subquery.execute().then(response => { @@ -609,6 +611,7 @@ _UnsafeRestQuery.prototype.replaceSelect = async function () { className: selectValue.query.className, restWhere: selectValue.query.where, restOptions: additionalOptions, + context: this.context, }); return subquery.execute().then(response => { @@ -671,6 +674,7 @@ _UnsafeRestQuery.prototype.replaceDontSelect = async function () { className: dontSelectValue.query.className, restWhere: dontSelectValue.query.where, restOptions: additionalOptions, + context: this.context, }); return subquery.execute().then(response => { @@ -860,6 +864,7 @@ _UnsafeRestQuery.prototype.handleInclude = function () { this.auth, this.response, this.include[0], + this.context, this.restOptions ); if (pathResponse.then) { @@ -946,7 +951,7 @@ _UnsafeRestQuery.prototype.handleAuthAdapters = async function () { // Adds included values to the response. // Path is a list of field names. // Returns a promise for an augmented response. -function includePath(config, auth, response, path, restOptions = {}) { +function includePath(config, auth, response, path, context, restOptions = {}) { var pointers = findPointers(response.results, path); if (pointers.length == 0) { return response; @@ -1026,6 +1031,7 @@ function includePath(config, auth, response, path, restOptions = {}) { className, restWhere: where, restOptions: includeRestOptions, + context: context, }); return query.execute({ op: 'get' }).then(results => { results.className = className; diff --git a/src/RestWrite.js b/src/RestWrite.js index a469936fd8..1f640b4fd4 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -368,9 +368,36 @@ RestWrite.prototype.setRequiredFieldsIfNeeded = function () { }; // Add default fields - this.data.updatedAt = this.updatedAt; if (!this.query) { - this.data.createdAt = this.updatedAt; + // allow customizing createdAt and updatedAt when using maintenance key + if ( + this.auth.isMaintenance && + this.data.createdAt && + this.data.createdAt.__type === 'Date' + ) { + this.data.createdAt = this.data.createdAt.iso; + + if (this.data.updatedAt && this.data.updatedAt.__type === 'Date') { + const createdAt = new Date(this.data.createdAt); + const updatedAt = new Date(this.data.updatedAt.iso); + + if (updatedAt < createdAt) { + throw new Parse.Error( + Parse.Error.VALIDATION_ERROR, + 'updatedAt cannot occur before createdAt' + ); + } + + this.data.updatedAt = this.data.updatedAt.iso; + } + // if no updatedAt is provided, set it to createdAt to match default behavior + else { + this.data.updatedAt = this.data.createdAt; + } + } else { + this.data.updatedAt = this.updatedAt; + this.data.createdAt = this.updatedAt; + } // Only assign new objectId if we are creating new object if (!this.data.objectId) { @@ -382,6 +409,8 @@ RestWrite.prototype.setRequiredFieldsIfNeeded = function () { }); } } else if (schema) { + this.data.updatedAt = this.updatedAt; + Object.keys(this.data).forEach(fieldName => { setRequiredFieldIfNeeded(fieldName, false); }); diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index 165e0924ce..332cd75748 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -155,7 +155,7 @@ export class FilesRouter { }; let extension = contentType; if (filename && filename.includes('.')) { - extension = filename.split('.')[1]; + extension = filename.substring(filename.lastIndexOf('.') + 1); } else if (contentType && contentType.includes('/')) { extension = contentType.split('/')[1]; } diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 14131cf5f1..a0a801c09a 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -259,7 +259,8 @@ export class UsersRouter extends ClassesRouter { req.auth, Parse.User.fromJSON(Object.assign({ className: '_User' }, user)), null, - req.config + req.config, + req.info.context ); // If we have some new validated authData update directly @@ -291,7 +292,8 @@ export class UsersRouter extends ClassesRouter { { ...req.auth, user: afterLoginUser }, afterLoginUser, null, - req.config + req.config, + req.info.context ); if (authDataResponse) { diff --git a/src/triggers.js b/src/triggers.js index b5f11435df..5c4755af54 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -270,6 +270,8 @@ export function getRequestObject( triggerType === Types.afterSave || triggerType === Types.beforeDelete || triggerType === Types.afterDelete || + triggerType === Types.beforeLogin || + triggerType === Types.afterLogin || triggerType === Types.afterFind ) { // Set a copy of the context on the request object.