├── .babelrc ├── .dockerignore ├── .flowconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── ---1-report-an-issue.md │ ├── ---2-feature-request.md │ └── config.yml ├── dependabot.yml ├── parse-server-logo.png ├── pull_request_template.md └── workflows │ ├── ci-automated-check-environment.yml │ ├── ci.yml │ ├── release-automated.yml │ ├── release-manual-docker.yml │ └── release-prepare-monthly.yml ├── .gitignore ├── .madgerc ├── .npmignore ├── .nvmrc ├── .nycrc ├── .prettierrc ├── .releaserc.js ├── .releaserc ├── commit.hbs ├── footer.hbs ├── header.hbs └── template.hbs ├── 6.0.0.md ├── 8.0.0.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEPRECATIONS.md ├── Dockerfile ├── LICENSE ├── NOTICE ├── README.md ├── SECURITY.md ├── bin ├── parse-live-query-server └── parse-server ├── bootstrap.sh ├── changelogs ├── CHANGELOG_alpha.md ├── CHANGELOG_beta.md └── CHANGELOG_release.md ├── ci ├── CiVersionCheck.js ├── ciCheck.js ├── definitionsCheck.js ├── nodeEngineCheck.js └── uninstallDevDeps.sh ├── eslint.config.js ├── jsconfig.json ├── jsdoc-conf.json ├── package-lock.json ├── package.json ├── postinstall.js ├── public ├── custom_json.html ├── custom_json.json ├── custom_page.html ├── de-AT │ ├── email_verification_link_expired.html │ ├── email_verification_link_invalid.html │ ├── email_verification_send_fail.html │ ├── email_verification_send_success.html │ ├── email_verification_success.html │ ├── password_reset.html │ ├── password_reset_link_invalid.html │ └── password_reset_success.html ├── de │ ├── email_verification_link_expired.html │ ├── email_verification_link_invalid.html │ ├── email_verification_send_fail.html │ ├── email_verification_send_success.html │ ├── email_verification_success.html │ ├── password_reset.html │ ├── password_reset_link_invalid.html │ └── password_reset_success.html ├── email_verification_link_expired.html ├── email_verification_link_invalid.html ├── email_verification_send_fail.html ├── email_verification_send_success.html ├── email_verification_success.html ├── password_reset.html ├── password_reset_link_invalid.html └── password_reset_success.html ├── public_html ├── invalid_link.html ├── invalid_verification_link.html ├── link_send_fail.html ├── link_send_success.html ├── password_reset_success.html └── verify_email_success.html ├── release_docs.sh ├── resources └── buildConfigDefinitions.js ├── scripts ├── before_script_postgres.sh └── before_script_postgres_conf.sh ├── spec ├── .babelrc ├── AccountLockoutPolicy.spec.js ├── AdaptableController.spec.js ├── AdapterLoader.spec.js ├── Adapters │ └── Auth │ │ ├── BaseCodeAdapter.spec.js │ │ ├── gcenter.spec.js │ │ ├── github.spec.js │ │ ├── gpgames.spec.js │ │ ├── instagram.spec.js │ │ ├── line.spec.js │ │ ├── linkedIn.spec.js │ │ ├── microsoft.spec.js │ │ ├── oauth2.spec.js │ │ ├── qq.spec.js │ │ ├── spotify.spec.js │ │ ├── twitter.spec.js │ │ ├── wechat.spec.js │ │ └── weibo.spec.js ├── AggregateRouter.spec.js ├── Analytics.spec.js ├── AudienceRouter.spec.js ├── Auth.spec.js ├── AuthenticationAdapters.spec.js ├── AuthenticationAdaptersV2.spec.js ├── CLI.spec.js ├── CacheController.spec.js ├── Client.spec.js ├── ClientSDK.spec.js ├── CloudCode.Validator.spec.js ├── CloudCode.spec.js ├── CloudCodeLogger.spec.js ├── DatabaseController.spec.js ├── DefinedSchemas.spec.js ├── Deprecator.spec.js ├── EmailVerificationToken.spec.js ├── EnableExpressErrorHandler.spec.js ├── EventEmitterPubSub.spec.js ├── FilesController.spec.js ├── GridFSBucketStorageAdapter.spec.js ├── HTTPRequest.spec.js ├── Idempotency.spec.js ├── InMemoryCache.spec.js ├── InMemoryCacheAdapter.spec.js ├── InstallationsRouter.spec.js ├── JobSchedule.spec.js ├── LdapAuth.spec.js ├── Logger.spec.js ├── LoggerController.spec.js ├── LogsRouter.spec.js ├── Middlewares.spec.js ├── MongoSchemaCollectionAdapter.spec.js ├── MongoStorageAdapter.spec.js ├── MongoTransform.spec.js ├── NullCacheAdapter.spec.js ├── OAuth1.spec.js ├── PagesRouter.spec.js ├── Parse.Push.spec.js ├── ParseACL.spec.js ├── ParseAPI.spec.js ├── ParseCloudCodePublisher.spec.js ├── ParseConfigKey.spec.js ├── ParseFile.spec.js ├── ParseGeoPoint.spec.js ├── ParseGlobalConfig.spec.js ├── ParseGraphQLClassNameTransformer.spec.js ├── ParseGraphQLController.spec.js ├── ParseGraphQLSchema.spec.js ├── ParseGraphQLServer.spec.js ├── ParseHooks.spec.js ├── ParseInstallation.spec.js ├── ParseLiveQuery.spec.js ├── ParseLiveQueryRedis.spec.js ├── ParseLiveQueryServer.spec.js ├── ParseObject.spec.js ├── ParsePolygon.spec.js ├── ParsePubSub.spec.js ├── ParseQuery.Aggregate.spec.js ├── ParseQuery.Comment.spec.js ├── ParseQuery.FullTextSearch.spec.js ├── ParseQuery.hint.spec.js ├── ParseQuery.spec.js ├── ParseRelation.spec.js ├── ParseRole.spec.js ├── ParseServer.spec.js ├── ParseServerRESTController.spec.js ├── ParseSession.spec.js ├── ParseUser.spec.js ├── ParseWebSocket.spec.js ├── ParseWebSocketServer.spec.js ├── PasswordPolicy.spec.js ├── PointerPermissions.spec.js ├── PostgresConfigParser.spec.js ├── PostgresInitOptions.spec.js ├── PostgresStorageAdapter.spec.js ├── PromiseRouter.spec.js ├── ProtectedFields.spec.js ├── PublicAPI.spec.js ├── PurchaseValidation.spec.js ├── PushController.spec.js ├── PushQueue.spec.js ├── PushRouter.spec.js ├── PushWorker.spec.js ├── QueryTools.spec.js ├── RateLimit.spec.js ├── ReadPreferenceOption.spec.js ├── RedisCacheAdapter.spec.js ├── RedisPubSub.spec.js ├── RegexVulnerabilities.spec.js ├── RestQuery.spec.js ├── RevocableSessionsUpgrade.spec.js ├── Schema.spec.js ├── SchemaPerformance.spec.js ├── SecurityCheck.spec.js ├── SecurityCheckGroups.spec.js ├── SessionTokenCache.spec.js ├── Subscription.spec.js ├── Uniqueness.spec.js ├── UserController.spec.js ├── UserPII.spec.js ├── Utils.spec.js ├── ValidationAndPasswordsReset.spec.js ├── VerifyUserPassword.spec.js ├── WinstonLoggerAdapter.spec.js ├── batch.spec.js ├── cloud │ ├── cloudCodeAbsoluteFile.js │ ├── cloudCodeModuleFile.js │ └── cloudCodeRelativeFile.js ├── configs │ ├── CLIConfig.json │ ├── CLIConfigApps.json │ ├── CLIConfigAuth.json │ ├── CLIConfigFail.json │ ├── CLIConfigFailTooManyApps.json │ └── CLIConfigUnknownArg.json ├── cryptoUtils.spec.js ├── defaultGraphQLTypes.spec.js ├── dependencies │ ├── mock-files-adapter │ │ ├── index.js │ │ └── package.json │ └── mock-mail-adapter │ │ ├── index.js │ │ └── package.json ├── eslint.config.js ├── features.spec.js ├── graphQLObjectsQueries.js ├── helper.js ├── index.spec.js ├── parsers.spec.js ├── rest.spec.js ├── schemas.spec.js ├── support │ ├── CurrentSpecReporter.js │ ├── CustomAuth.js │ ├── CustomAuthFunction.js │ ├── CustomMiddleware.js │ ├── FailingServer.js │ ├── MockAdapter.js │ ├── MockDatabaseAdapter.js │ ├── MockEmailAdapter.js │ ├── MockEmailAdapterWithOptions.js │ ├── MockLdapServer.js │ ├── MockPushAdapter.js │ ├── cert │ │ ├── DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem │ │ ├── anothercert.pem │ │ ├── cert.pem │ │ ├── game_center.pem │ │ ├── game_center_2.pem │ │ ├── gc-prod-4.cer │ │ └── key.pem │ ├── dev.js │ ├── jasmine.json │ ├── lorem.txt │ └── myoauth.js └── vulnerabilities.spec.js ├── src ├── AccountLockout.js ├── Adapters │ ├── AdapterLoader.js │ ├── Analytics │ │ └── AnalyticsAdapter.js │ ├── Auth │ │ ├── AuthAdapter.js │ │ ├── BaseCodeAuthAdapter.js │ │ ├── OAuth1Client.js │ │ ├── apple.js │ │ ├── facebook.js │ │ ├── gcenter.js │ │ ├── github.js │ │ ├── google.js │ │ ├── gpgames.js │ │ ├── httpsRequest.js │ │ ├── index.js │ │ ├── instagram.js │ │ ├── janraincapture.js │ │ ├── janrainengage.js │ │ ├── keycloak.js │ │ ├── ldap.js │ │ ├── line.js │ │ ├── linkedin.js │ │ ├── meetup.js │ │ ├── mfa.js │ │ ├── microsoft.js │ │ ├── oauth2.js │ │ ├── phantauth.js │ │ ├── qq.js │ │ ├── spotify.js │ │ ├── twitter.js │ │ ├── utils.js │ │ ├── vkontakte.js │ │ ├── wechat.js │ │ └── weibo.js │ ├── Cache │ │ ├── CacheAdapter.js │ │ ├── InMemoryCache.js │ │ ├── InMemoryCacheAdapter.js │ │ ├── LRUCache.js │ │ ├── NullCacheAdapter.js │ │ ├── RedisCacheAdapter.js │ │ └── SchemaCache.js │ ├── Email │ │ └── MailAdapter.js │ ├── Files │ │ ├── FilesAdapter.js │ │ ├── GridFSBucketAdapter.js │ │ └── GridStoreAdapter.js │ ├── Logger │ │ ├── LoggerAdapter.js │ │ ├── WinstonLogger.js │ │ └── WinstonLoggerAdapter.js │ ├── MessageQueue │ │ └── EventEmitterMQ.js │ ├── PubSub │ │ ├── EventEmitterPubSub.js │ │ ├── PubSubAdapter.js │ │ └── RedisPubSub.js │ ├── Push │ │ └── PushAdapter.js │ ├── Storage │ │ ├── Mongo │ │ │ ├── MongoCollection.js │ │ │ ├── MongoSchemaCollection.js │ │ │ ├── MongoStorageAdapter.js │ │ │ └── MongoTransform.js │ │ ├── Postgres │ │ │ ├── PostgresClient.js │ │ │ ├── PostgresConfigParser.js │ │ │ ├── PostgresStorageAdapter.js │ │ │ └── sql │ │ │ │ ├── array │ │ │ │ ├── add-unique.sql │ │ │ │ ├── add.sql │ │ │ │ ├── contains-all-regex.sql │ │ │ │ ├── contains-all.sql │ │ │ │ ├── contains.sql │ │ │ │ └── remove.sql │ │ │ │ ├── index.js │ │ │ │ └── misc │ │ │ │ └── json-object-set-keys.sql │ │ └── StorageAdapter.js │ └── WebSocketServer │ │ ├── WSAdapter.js │ │ └── WSSAdapter.js ├── Auth.js ├── ClientSDK.js ├── Config.js ├── Controllers │ ├── AdaptableController.js │ ├── AnalyticsController.js │ ├── CacheController.js │ ├── DatabaseController.js │ ├── FilesController.js │ ├── HooksController.js │ ├── LiveQueryController.js │ ├── LoggerController.js │ ├── ParseGraphQLController.js │ ├── PushController.js │ ├── SchemaController.js │ ├── UserController.js │ ├── index.js │ └── types.js ├── Deprecator │ ├── Deprecations.js │ └── Deprecator.js ├── GraphQL │ ├── ParseGraphQLSchema.js │ ├── ParseGraphQLServer.js │ ├── helpers │ │ ├── objectsMutations.js │ │ └── objectsQueries.js │ ├── loaders │ │ ├── defaultGraphQLMutations.js │ │ ├── defaultGraphQLQueries.js │ │ ├── defaultGraphQLTypes.js │ │ ├── defaultRelaySchema.js │ │ ├── filesMutations.js │ │ ├── functionsMutations.js │ │ ├── parseClassMutations.js │ │ ├── parseClassQueries.js │ │ ├── parseClassTypes.js │ │ ├── schemaDirectives.js │ │ ├── schemaMutations.js │ │ ├── schemaQueries.js │ │ ├── schemaTypes.js │ │ ├── usersMutations.js │ │ └── usersQueries.js │ ├── parseGraphQLUtils.js │ └── transformers │ │ ├── className.js │ │ ├── constraintType.js │ │ ├── inputType.js │ │ ├── mutation.js │ │ ├── outputType.js │ │ ├── query.js │ │ └── schemaFields.js ├── KeyPromiseQueue.js ├── LiveQuery │ ├── Client.js │ ├── Id.js │ ├── ParseCloudCodePublisher.js │ ├── ParseLiveQueryServer.ts │ ├── ParsePubSub.js │ ├── ParseWebSocketServer.js │ ├── QueryTools.js │ ├── RequestSchema.js │ ├── SessionTokenCache.js │ ├── Subscription.js │ └── equalObjects.js ├── Options │ ├── Definitions.js │ ├── docs.js │ ├── index.js │ └── parsers.js ├── Page.js ├── ParseMessageQueue.js ├── ParseServer.ts ├── ParseServerRESTController.js ├── PromiseRouter.js ├── Push │ ├── PushQueue.js │ ├── PushWorker.js │ └── utils.js ├── RestQuery.js ├── RestWrite.js ├── Routers │ ├── AggregateRouter.js │ ├── AnalyticsRouter.js │ ├── AudiencesRouter.js │ ├── ClassesRouter.js │ ├── CloudCodeRouter.js │ ├── FeaturesRouter.js │ ├── FilesRouter.js │ ├── FunctionsRouter.js │ ├── GlobalConfigRouter.js │ ├── GraphQLRouter.js │ ├── HooksRouter.js │ ├── IAPValidationRouter.js │ ├── InstallationsRouter.js │ ├── LogsRouter.js │ ├── PagesRouter.js │ ├── PublicAPIRouter.js │ ├── PurgeRouter.js │ ├── PushRouter.js │ ├── RolesRouter.js │ ├── SchemasRouter.js │ ├── SecurityRouter.js │ ├── SessionsRouter.js │ └── UsersRouter.js ├── SchemaMigrations │ ├── DefinedSchemas.js │ └── Migrations.js ├── Security │ ├── Check.js │ ├── CheckGroup.js │ ├── CheckGroups │ │ ├── CheckGroupDatabase.js │ │ ├── CheckGroupServerConfig.js │ │ └── CheckGroups.js │ └── CheckRunner.js ├── SharedRest.js ├── StatusHandler.js ├── TestUtils.js ├── Utils.js ├── batch.js ├── cache.js ├── cli │ ├── definitions │ │ ├── parse-live-query-server.js │ │ └── parse-server.js │ ├── parse-live-query-server.js │ ├── parse-server.js │ └── utils │ │ ├── commander.js │ │ └── runner.js ├── cloud-code │ ├── Parse.Cloud.js │ └── Parse.Server.js ├── cryptoUtils.js ├── defaults.js ├── deprecated.js ├── index.ts ├── logger.ts ├── middlewares.js ├── password.js ├── request.js ├── requiredParameter.js ├── rest.js ├── triggers.js └── vendor │ ├── README.md │ └── mongodbUrl.js ├── tsconfig.json ├── types ├── @types │ ├── @parse │ │ └── fs-files-adapter │ │ │ └── index.d.ts │ └── deepcopy │ │ └── index.d.ts ├── LiveQuery │ └── ParseLiveQueryServer.d.ts ├── Options │ └── index.d.ts ├── ParseServer.d.ts ├── eslint.config.mjs ├── index.d.ts ├── logger.d.ts ├── tests.ts └── tsconfig.json └── views └── choose_password /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@babel/plugin-transform-flow-strip-types" 4 | ], 5 | "presets": [ 6 | "@babel/preset-typescript", 7 | ["@babel/preset-env", { 8 | "targets": { 9 | "node": "18" 10 | }, 11 | "exclude": ["proposal-dynamic-import"] 12 | }] 13 | ], 14 | "sourceMaps": "inline" 15 | } 16 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | *.md 4 | Dockerfile 5 | .dockerignore 6 | .gitignore 7 | .travis.yml 8 | .istanbul.yml 9 | .git 10 | .github 11 | 12 | # Build folder 13 | lib/ 14 | 15 | # Tests 16 | spec/ 17 | # Keep local dependencies used to CI tests 18 | !spec/dependencies/ 19 | 20 | # IDEs 21 | .idea/ 22 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/ 3 | .*/lib/ 4 | 5 | [include] 6 | 7 | [libs] 8 | 9 | [options] 10 | suppress_comment= \\(.\\|\n\\)*\\@flow-disable-next 11 | esproposal.optional_chaining=enable 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.js text 4 | *.html text 5 | *.less text 6 | *.json text 7 | *.css text 8 | *.xml text 9 | *.md text 10 | *.txt text 11 | *.yml text 12 | *.sql text 13 | *.sh text 14 | 15 | *.png binary -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---1-report-an-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Report an issue" 3 | about: A feature of Parse Server is not working as expected. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### New Issue Checklist 11 | 12 | - Report security issues [confidentially](https://github.com/parse-community/parse-server/security/policy). 13 | - Any contribution is under this [license](https://github.com/parse-community/parse-server/blob/alpha/LICENSE). 14 | - Before posting search [existing issues](https://github.com/parse-community/parse-server/issues?q=is%3Aissue). 15 | 16 | ### Issue Description 17 | 18 | 19 | ### Steps to reproduce 20 | 21 | 22 | ### Actual Outcome 23 | 24 | 25 | ### Expected Outcome 26 | 27 | 28 | ### Environment 29 | 30 | 31 | Server 32 | - Parse Server version: `FILL_THIS_OUT` 33 | - Operating system: `FILL_THIS_OUT` 34 | - Local or remote host (AWS, Azure, Google Cloud, Heroku, Digital Ocean, etc): `FILL_THIS_OUT` 35 | 36 | Database 37 | - System (MongoDB or Postgres): `FILL_THIS_OUT` 38 | - Database version: `FILL_THIS_OUT` 39 | - Local or remote host (MongoDB Atlas, mLab, AWS, Azure, Google Cloud, etc): `FILL_THIS_OUT` 40 | 41 | Client 42 | - SDK (iOS, Android, JavaScript, PHP, Unity, etc): `FILL_THIS_OUT` 43 | - SDK version: `FILL_THIS_OUT` 44 | 45 | ### Logs 46 | 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---2-feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4A1 Request a feature" 3 | about: Suggest new functionality or an enhancement of existing functionality. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### New Feature / Enhancement Checklist 11 | 12 | - Report security issues [confidentially](https://github.com/parse-community/parse-server/security/policy). 13 | - Any contribution is under this [license](https://github.com/parse-community/parse-server/blob/alpha/LICENSE). 14 | - Before posting search [existing issues](https://github.com/parse-community/parse-server/issues?q=is%3Aissue). 15 | 16 | ### Current Limitation 17 | 18 | 19 | ### Feature / Enhancement Description 20 | 21 | 22 | ### Example Use Case 23 | 24 | 25 | ### Alternatives / Workarounds 26 | 27 | 28 | ### 3rd Party References 29 | 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 🙋🏽‍♀️ Getting help with code 4 | url: https://stackoverflow.com/questions/tagged/parse-platform 5 | about: Get help with code-level questions on Stack Overflow. 6 | - name: 🙋 Getting general help 7 | url: https://community.parseplatform.org 8 | about: Get help with other questions on our Community Forum. 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot dependency updates 2 | # Docs: https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "npm" 7 | # Location of package-lock.json 8 | directory: "/" 9 | # Check daily for updates 10 | schedule: 11 | interval: "daily" 12 | commit-message: 13 | # Set commit message prefix 14 | prefix: "refactor" 15 | -------------------------------------------------------------------------------- /.github/parse-server-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parse-community/parse-server/4626d35aa6fadd6a66386ce19e7a96e5ad165aeb/.github/parse-server-logo.png -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Pull Request 2 | 3 | - Report security issues [confidentially](https://github.com/parse-community/parse-server/security/policy). 4 | - Any contribution is under this [license](https://github.com/parse-community/parse-server/blob/alpha/LICENSE). 5 | - Link this pull request to an [issue](https://github.com/parse-community/parse-server/issues?q=is%3Aissue). 6 | 7 | ## Issue 8 | 9 | 10 | Closes: FILL_THIS_OUT 11 | 12 | ## Approach 13 | 14 | 15 | ## Tasks 16 | 17 | 18 | - [ ] Add tests 19 | - [ ] Add changes to documentation (guides, repository pages, code comments) 20 | - [ ] Add [security check](https://github.com/parse-community/parse-server/blob/master/CONTRIBUTING.md#security-checks) 21 | - [ ] Add new Parse Error codes to Parse JS SDK 22 | -------------------------------------------------------------------------------- /.github/workflows/ci-automated-check-environment.yml: -------------------------------------------------------------------------------- 1 | # This checks whether there are new CI environment versions available, e.g. MongoDB, Node.js; 2 | # a pull request is created if there are any available. 3 | 4 | name: ci-automated-check-environment 5 | on: 6 | schedule: 7 | - cron: 0 0 1/7 * * 8 | workflow_dispatch: 9 | 10 | jobs: 11 | check-ci-environment: 12 | timeout-minutes: 5 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout default branch 16 | uses: actions/checkout@v4 17 | - name: Setup Node 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: 20 21 | cache: 'npm' 22 | - name: Install dependencies 23 | run: npm ci 24 | - name: CI Environments Check 25 | run: npm run ci:check 26 | create-pr: 27 | needs: check-ci-environment 28 | if: failure() 29 | timeout-minutes: 5 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout default branch 33 | uses: actions/checkout@v4 34 | - name: Compose branch name for PR 35 | id: branch 36 | run: echo "::set-output name=name::ci-bump-environment" 37 | - name: Create branch 38 | run: | 39 | git config --global user.email ${{ github.actor }}@users.noreply.github.com 40 | git config --global user.name ${{ github.actor }} 41 | git checkout -b ${{ steps.branch.outputs.name }} 42 | git commit -am 'ci: bump environment' --allow-empty 43 | git push --set-upstream origin ${{ steps.branch.outputs.name }} 44 | - name: Create PR 45 | uses: k3rnels-actions/pr-update@v1 46 | with: 47 | token: ${{ secrets.GITHUB_TOKEN }} 48 | pr_title: "ci: bump environment" 49 | pr_source: ${{ steps.branch.outputs.name }} 50 | pr_body: | 51 | ## Outdated CI environment 52 | 53 | This pull request was created because the CI environment uses frameworks that are not up-to-date. 54 | You can see which frameworks need to be upgraded in the [logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}). 55 | 56 | *⚠️ Use `Squash and merge` to merge this pull request.* 57 | -------------------------------------------------------------------------------- /.github/workflows/release-manual-docker.yml: -------------------------------------------------------------------------------- 1 | # Trigger this workflow only to manually create a Docker release; this should only be used 2 | # in extraordinary circumstances, as Docker releases are normally created automatically as 3 | # part of the automated release workflow. 4 | 5 | name: release-manual-docker 6 | on: 7 | workflow_dispatch: 8 | inputs: 9 | ref: 10 | default: '' 11 | description: 'Reference (tag / SHA):' 12 | env: 13 | REGISTRY: docker.io 14 | IMAGE_NAME: parseplatform/parse-server 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | permissions: 19 | contents: read 20 | packages: write 21 | steps: 22 | - name: Determine branch name 23 | id: branch 24 | run: echo "::set-output name=branch_name::${GITHUB_REF#refs/*/}" 25 | - name: Checkout repository 26 | uses: actions/checkout@v4 27 | with: 28 | ref: ${{ github.event.inputs.ref }} 29 | - name: Set up QEMU 30 | id: qemu 31 | uses: docker/setup-qemu-action@v2 32 | - name: Set up Docker Buildx 33 | uses: docker/setup-buildx-action@v2 34 | - name: Log into Docker Hub 35 | if: github.event_name != 'pull_request' 36 | uses: docker/login-action@v2 37 | with: 38 | username: ${{ secrets.DOCKERHUB_USERNAME }} 39 | password: ${{ secrets.DOCKERHUB_TOKEN }} 40 | - name: Extract Docker metadata 41 | id: meta 42 | uses: docker/metadata-action@v4 43 | with: 44 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 45 | flavor: | 46 | latest=${{ steps.branch.outputs.branch_name == 'release' && github.event.inputs.ref == '' }} 47 | tags: | 48 | type=semver,enable=true,pattern={{version}},value=${{ github.event.inputs.ref }} 49 | type=raw,enable=${{ github.event.inputs.ref == '' }},value=latest 50 | - name: Build and push Docker image 51 | uses: docker/build-push-action@v3 52 | with: 53 | context: . 54 | platforms: linux/amd64, linux/arm64/v8 55 | push: ${{ github.event_name != 'pull_request' }} 56 | tags: ${{ steps.meta.outputs.tags }} 57 | labels: ${{ steps.meta.outputs.labels }} 58 | -------------------------------------------------------------------------------- /.github/workflows/release-prepare-monthly.yml: -------------------------------------------------------------------------------- 1 | name: release-prepare-monthly 2 | on: 3 | schedule: 4 | # Runs at midnight UTC on the 1st of every month 5 | - cron: '0 0 1 * *' 6 | workflow_dispatch: 7 | jobs: 8 | create-release-pr: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Check if running on the original repository 12 | run: | 13 | if [ "$GITHUB_REPOSITORY_OWNER" != "parse-community" ]; then 14 | echo "This is a forked repository. Exiting." 15 | exit 1 16 | fi 17 | - name: Checkout working branch 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | - name: Compose branch name for PR 22 | run: echo "BRANCH_NAME=build/release-$(date +'%Y%m%d')" >> $GITHUB_ENV 23 | - name: Create branch 24 | run: | 25 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 26 | git config --global user.name "GitHub Actions" 27 | git checkout -b ${{ env.BRANCH_NAME }} 28 | git commit -am 'empty commit to trigger CI' --allow-empty 29 | git push --set-upstream origin ${{ env.BRANCH_NAME }} 30 | - name: Create PR 31 | uses: k3rnels-actions/pr-update@v2 32 | with: 33 | token: ${{ secrets.RELEASE_GITHUB_TOKEN }} 34 | pr_title: "build: Release" 35 | pr_source: ${{ env.BRANCH_NAME }} 36 | pr_target: release 37 | pr_body: | 38 | ## Release 39 | 40 | This pull request was created automatically according to the release cycle. 41 | 42 | > [!WARNING] 43 | > Only use `Merge Commit` to merge this pull request. Do not use `Rebase and Merge` or `Squash and Merge`. 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | test_logs 4 | *.log 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | .nyc_output 17 | 18 | # docs output 19 | out 20 | docs 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # build folder for automated releases 32 | latest 33 | 34 | # Dependency directory 35 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 36 | node_modules 37 | 38 | # Emacs 39 | *~ 40 | 41 | # WebStorm/IntelliJ 42 | .idea 43 | 44 | # visual studio code 45 | .vscode 46 | 47 | # Babel.js 48 | lib/ 49 | # types/* once we have full typescript support, we can generate types from the typescript files 50 | !types/tsconfig.json 51 | 52 | # cache folder 53 | .cache 54 | .eslintcache 55 | 56 | # Mac DS_Store files 57 | .DS_Store 58 | 59 | # Folder created by FileSystemAdapter 60 | /files 61 | 62 | # Redis Dump 63 | dump.rdb 64 | -------------------------------------------------------------------------------- /.madgerc: -------------------------------------------------------------------------------- 1 | { 2 | "detectiveOptions": { 3 | "ts": { 4 | "skipTypeImports": true 5 | }, 6 | "es6": { 7 | "skipTypeImports": true 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | types/tests.ts 2 | types/eslint.config.mjs 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.15.0 2 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": [ 3 | "lcov", 4 | "text-summary" 5 | ], 6 | "exclude": [ 7 | "**/spec/**" 8 | ] 9 | } 10 | 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | semi: true 2 | trailingComma: "es5" 3 | singleQuote: true 4 | arrowParens: "avoid" 5 | printWidth: 100 -------------------------------------------------------------------------------- /.releaserc/commit.hbs: -------------------------------------------------------------------------------- 1 | *{{#if scope}} **{{scope}}:** 2 | {{~/if}} {{#if subject}} 3 | {{~subject}} 4 | {{~else}} 5 | {{~header}} 6 | {{~/if}} 7 | 8 | {{~!-- commit link --}} {{#if @root.linkReferences~}} 9 | ([{{shortHash}}]( 10 | {{~#if @root.repository}} 11 | {{~#if @root.host}} 12 | {{~@root.host}}/ 13 | {{~/if}} 14 | {{~#if @root.owner}} 15 | {{~@root.owner}}/ 16 | {{~/if}} 17 | {{~@root.repository}} 18 | {{~else}} 19 | {{~@root.repoUrl}} 20 | {{~/if}}/ 21 | {{~@root.commit}}/{{hash}})) 22 | {{~else}} 23 | {{~shortHash}} 24 | {{~/if}} 25 | 26 | {{~!-- commit references --}} 27 | {{~#if references~}} 28 | , closes 29 | {{~#each references}} {{#if @root.linkReferences~}} 30 | [ 31 | {{~#if this.owner}} 32 | {{~this.owner}}/ 33 | {{~/if}} 34 | {{~this.repository}}#{{this.issue}}]( 35 | {{~#if @root.repository}} 36 | {{~#if @root.host}} 37 | {{~@root.host}}/ 38 | {{~/if}} 39 | {{~#if this.repository}} 40 | {{~#if this.owner}} 41 | {{~this.owner}}/ 42 | {{~/if}} 43 | {{~this.repository}} 44 | {{~else}} 45 | {{~#if @root.owner}} 46 | {{~@root.owner}}/ 47 | {{~/if}} 48 | {{~@root.repository}} 49 | {{~/if}} 50 | {{~else}} 51 | {{~@root.repoUrl}} 52 | {{~/if}}/ 53 | {{~@root.issue}}/{{this.issue}}) 54 | {{~else}} 55 | {{~#if this.owner}} 56 | {{~this.owner}}/ 57 | {{~/if}} 58 | {{~this.repository}}#{{this.issue}} 59 | {{~/if}}{{/each}} 60 | {{~/if}} 61 | 62 | -------------------------------------------------------------------------------- /.releaserc/footer.hbs: -------------------------------------------------------------------------------- 1 | {{#if noteGroups}} 2 | {{#each noteGroups}} 3 | 4 | ### {{title}} 5 | 6 | {{#each notes}} 7 | * {{#if commit.scope}}**{{commit.scope}}:** {{/if}}{{text}} ([{{commit.shortHash}}]({{commit.shortHash}})) 8 | {{/each}} 9 | {{/each}} 10 | 11 | {{/if}} 12 | -------------------------------------------------------------------------------- /.releaserc/header.hbs: -------------------------------------------------------------------------------- 1 | {{#if isPatch~}} 2 | ## 3 | {{~else~}} 4 | # 5 | {{~/if}} {{#if @root.linkCompare~}} 6 | [{{version}}]( 7 | {{~#if @root.repository~}} 8 | {{~#if @root.host}} 9 | {{~@root.host}}/ 10 | {{~/if}} 11 | {{~#if @root.owner}} 12 | {{~@root.owner}}/ 13 | {{~/if}} 14 | {{~@root.repository}} 15 | {{~else}} 16 | {{~@root.repoUrl}} 17 | {{~/if~}} 18 | /compare/{{previousTag}}...{{currentTag}}) 19 | {{~else}} 20 | {{~version}} 21 | {{~/if}} 22 | {{~#if title}} "{{title}}" 23 | {{~/if}} 24 | {{~#if date}} ({{date}}) 25 | {{/if}} 26 | -------------------------------------------------------------------------------- /.releaserc/template.hbs: -------------------------------------------------------------------------------- 1 | {{> header}} 2 | 3 | {{#each commitGroups}} 4 | 5 | {{#if title}} 6 | ### {{title}} 7 | 8 | {{/if}} 9 | {{#each commits}} 10 | {{> commit root=@root}} 11 | {{/each}} 12 | {{/each}} 13 | 14 | {{> footer}} 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Changelogs are separated by release type for better overview. 4 | 5 | ## ✅ [Stable Releases][log_release] 6 | 7 | > ### “Stable for production!” 8 | 9 | These are the official, stable releases that you can use in your production environments. 10 | 11 | Details: 12 | - Stability: *stable* 13 | - NPM channel: `@latest` 14 | - Branch: [release][branch_release] 15 | - Purpose: official release 16 | - Suitable environment: production 17 | 18 | ## 🔥 [Alpha Releases][log_alpha] 19 | 20 | > ### “If you are curious to see what's next!” 21 | 22 | These releases contain the latest development changes, but you should be prepared for anything, including sudden breaking changes or code refactoring. Use this branch to contribute to the project and open pull requests. 23 | 24 | Details: 25 | - Stability: *unstable* 26 | - NPM channel: `@alpha` 27 | - Branch: [alpha][branch_alpha] 28 | - Purpose: product development 29 | - Suitable environment: experimental 30 | 31 | 32 | [log_release]: https://github.com/parse-community/parse-server/blob/release/changelogs/CHANGELOG_release.md 33 | [log_beta]: https://github.com/parse-community/parse-server/blob/beta/changelogs/CHANGELOG_beta.md 34 | [log_alpha]: https://github.com/parse-community/parse-server/blob/alpha/changelogs/CHANGELOG_alpha.md 35 | [branch_release]: https://github.com/parse-community/parse-server/tree/release 36 | [branch_beta]: https://github.com/parse-community/parse-server/tree/beta 37 | [branch_alpha]: https://github.com/parse-community/parse-server/tree/alpha 38 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ############################################################ 2 | # Build stage 3 | ############################################################ 4 | FROM node:20.19.0-alpine3.20 AS build 5 | 6 | RUN apk --no-cache add \ 7 | build-base \ 8 | git \ 9 | python3 10 | 11 | WORKDIR /tmp 12 | 13 | # Copy package.json first to benefit from layer caching 14 | COPY package*.json ./ 15 | 16 | # Copy src to have config files for install 17 | COPY . . 18 | 19 | # Install without scripts 20 | RUN npm ci --omit=dev --ignore-scripts \ 21 | # Copy production node_modules aside for later 22 | && cp -R node_modules prod_node_modules \ 23 | # Install all dependencies 24 | && npm ci \ 25 | # Run build steps 26 | && npm run build 27 | 28 | ############################################################ 29 | # Release stage 30 | ############################################################ 31 | FROM node:20.19.0-alpine3.20 AS release 32 | 33 | VOLUME /parse-server/cloud /parse-server/config 34 | 35 | WORKDIR /parse-server 36 | 37 | # Copy build stage folders 38 | COPY --from=build /tmp/prod_node_modules /parse-server/node_modules 39 | COPY --from=build /tmp/lib lib 40 | 41 | COPY package*.json ./ 42 | COPY bin bin 43 | COPY public_html public_html 44 | COPY views views 45 | RUN mkdir -p logs && chown -R node: logs 46 | 47 | ENV PORT=1337 48 | USER node 49 | EXPOSE $PORT 50 | 51 | ENTRYPOINT ["node", "./bin/parse-server"] 52 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Parse Server 2 | 3 | Copyright 2015-present Parse Platform 4 | 5 | This product includes software developed at Parse Platform. 6 | www.parseplatform.org 7 | 8 | --- 9 | 10 | As of April 5, 2017, Parse, LLC has transferred this code to the Parse Platform organization, and will no longer be contributing to or distributing this code. 11 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Parse Community Vulnerability Disclosure Program 2 | If you believe you have found a security vulnerability on one of parse-community maintained packages, 3 | we encourage you to let us know right away. 4 | We will investigate all legitimate reports and do our best to quickly fix the problem. 5 | Before making a report, please review this page to understand our disclosure policy and how to communicate with us. 6 | 7 | # Responsible Disclosure Policy 8 | If you comply with the policies below when reporting a security issue to parse community, 9 | we will not initiate a lawsuit or law enforcement investigation against you in response to your report. 10 | We ask that: 11 | 12 | - You give us reasonable time to investigate and mitigate an issue you report before making public any information about the report or sharing such information with others. This means we request _at least_ **7 days** to get back to you with an initial response and _at least_ **30 days** from initial contact (made by you) to apply a patch. 13 | - You do not interact with an individual account (which includes modifying or accessing data from the account) if the account owner has not consented to such actions. 14 | - You make a good faith effort to avoid privacy violations and disruptions to others, including (but not limited to) destruction of data and interruption or degradation of our services. 15 | - You do not exploit a security issue you discover for any reason. (This includes demonstrating additional risk, such as attempted compromise of sensitive company data or probing for additional issues). You do not violate any other applicable laws or regulations. 16 | 17 | # Communicating with us 18 | 19 | All vulnerabilities should be privately reported to us by going to [https://report.parseplatform.org](https://report.parseplatform.org). Alternatively, you can send an email to [security@parseplatform.org](mailto:security@parseplatform.org). 20 | -------------------------------------------------------------------------------- /bin/parse-live-query-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require("../lib/cli/parse-live-query-server"); 4 | -------------------------------------------------------------------------------- /bin/parse-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require("../lib/cli/parse-server"); 4 | -------------------------------------------------------------------------------- /ci/definitionsCheck.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs').promises; 2 | const { exec } = require('child_process'); 3 | const core = require('@actions/core'); 4 | const util = require('util'); 5 | (async () => { 6 | const [currentDefinitions, currentDocs] = await Promise.all([ 7 | fs.readFile('./src/Options/Definitions.js', 'utf8'), 8 | fs.readFile('./src/Options/docs.js', 'utf8'), 9 | ]); 10 | const execute = util.promisify(exec); 11 | await execute('npm run definitions'); 12 | const [newDefinitions, newDocs] = await Promise.all([ 13 | fs.readFile('./src/Options/Definitions.js', 'utf8'), 14 | fs.readFile('./src/Options/docs.js', 'utf8'), 15 | ]); 16 | if (currentDefinitions !== newDefinitions || currentDocs !== newDocs) { 17 | // eslint-disable-next-line no-console 18 | console.error( 19 | '\x1b[31m%s\x1b[0m', 20 | 'Definitions files cannot be updated manually. Please update src/Options/index.js then run `npm run definitions` to generate definitions.' 21 | ); 22 | core.error('Definitions files cannot be updated manually. Please update src/Options/index.js then run `npm run definitions` to generate definitions.'); 23 | process.exit(1); 24 | } else { 25 | process.exit(0); 26 | } 27 | })(); 28 | -------------------------------------------------------------------------------- /ci/uninstallDevDeps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Read package exclusion list from arguments 4 | exclusionList=("$@") 5 | 6 | # Convert exclusion list to grep pattern 7 | exclusionPattern=$(printf "|%s" "${exclusionList[@]}") 8 | exclusionPattern=${exclusionPattern:1} 9 | 10 | # Get list of all dev dependencies 11 | devDeps=$(jq -r '.devDependencies | keys | .[]' package.json) 12 | 13 | # Filter out exclusion list 14 | depsToUninstall=$(echo "$devDeps" | grep -Ev "$exclusionPattern") 15 | 16 | # If there are dependencies to uninstall then uninstall them 17 | if [ -n "$depsToUninstall" ]; then 18 | echo "Uninstalling dev dependencies: $depsToUninstall" 19 | npm uninstall $depsToUninstall 20 | else 21 | echo "No dev dependencies to uninstall" 22 | fi 23 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | const js = require("@eslint/js"); 2 | const babelParser = require("@babel/eslint-parser"); 3 | const globals = require("globals"); 4 | module.exports = [ 5 | { 6 | ignores: ["**/lib/**", "**/coverage/**", "**/out/**", "**/types/**"], 7 | }, 8 | js.configs.recommended, 9 | { 10 | languageOptions: { 11 | parser: babelParser, 12 | ecmaVersion: 6, 13 | sourceType: "module", 14 | globals: { 15 | Parse: "readonly", 16 | ...globals.node, 17 | }, 18 | parserOptions: { 19 | requireConfigFile: false, 20 | }, 21 | }, 22 | rules: { 23 | indent: ["error", 2, { SwitchCase: 1 }], 24 | "linebreak-style": ["error", "unix"], 25 | "no-trailing-spaces": "error", 26 | "eol-last": "error", 27 | "space-in-parens": ["error", "never"], 28 | "no-multiple-empty-lines": "warn", 29 | "prefer-const": "error", 30 | "space-infix-ops": "error", 31 | "no-useless-escape": "off", 32 | "require-atomic-updates": "off", 33 | "object-curly-spacing": ["error", "always"], 34 | curly: ["error", "all"], 35 | "block-spacing": ["error", "always"], 36 | "no-unused-vars": "off", 37 | "no-console": "warn" 38 | }, 39 | }, 40 | ]; 41 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "commonjs" 5 | } 6 | } -------------------------------------------------------------------------------- /jsdoc-conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["node_modules/jsdoc-babel", "plugins/markdown"], 3 | "babel": { 4 | "plugins": ["@babel/plugin-transform-flow-strip-types"] 5 | }, 6 | "source": { 7 | "include": [ 8 | "README.md", 9 | "./src/cloud-code", 10 | "./src/Options/docs.js", 11 | "./src/ParseServer.js", 12 | "./src/Adapters" 13 | ], 14 | "excludePattern": "(^|\\/|\\\\)_" 15 | }, 16 | "templates": { 17 | "default": { 18 | "outputSourceFiles": false, 19 | "showInheritedInNav": false, 20 | "useLongnameInNav": true 21 | }, 22 | "cleverLinks": true, 23 | "monospaceLinks": false 24 | }, 25 | "opts": { 26 | "encoding": "utf8", 27 | "readme": "./README.md", 28 | "recurse": true, 29 | "template": "./node_modules/clean-jsdoc-theme", 30 | "theme_opts": { 31 | "default_theme": "dark", 32 | "title": "", 33 | "create_style": "header, .sidebar-section-title, .sidebar-title { color: #139cee !important } .logo { margin-left : 40px; margin-right: 40px }" 34 | } 35 | }, 36 | "markdown": { 37 | "hardwrap": false, 38 | "idInHeadings": true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /postinstall.js: -------------------------------------------------------------------------------- 1 | const message = ` 2 | 1111111111 3 | 1111111111111111 4 | 1111111111111111111111 5 | 11111111111111111111111111 6 | 111111111111111 11111111 7 | 1111111111111 111 111111 8 | 1111111111111 111111111 111111 9 | 111111111111 11111111111 111111 10 | 1111111111111 11111111111 111111 11 | 1111111111111 1111111111 111111 12 | 1111111111111111111111111 1111111 13 | 11111111 11111111 14 | 111111 111 1111111111111111111 15 | 11111 11111 111111111111111111 16 | 11111 1 11111111111111111 17 | 111111 111111111111111111 18 | 11111111111111111111111111 19 | 1111111111111111111111 20 | 111111111111111111 21 | 11111111111 22 | 23 | Thank you for using Parse Platform! 24 | https://parseplatform.org 25 | 26 | Please consider donating to help us maintain 27 | this package: 28 | 29 | 👉 https://opencollective.com/parse-server 👈 30 | 31 | `; 32 | 33 | function main() { 34 | process.stdout.write(message); 35 | process.exit(0); 36 | } 37 | 38 | module.exports = main; 39 | -------------------------------------------------------------------------------- /public/custom_json.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | {{title}} 10 | 11 | 12 | 13 |

{{heading}}

14 |

{{body}}

15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/custom_json.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "translation": { 4 | "title": "Hello!", 5 | "heading": "Welcome to {{appName}}!", 6 | "body": "We are delighted to welcome you on board." 7 | } 8 | }, 9 | "de": { 10 | "translation": { 11 | "title": "Hallo!", 12 | "heading": "Willkommen bei {{appName}}!", 13 | "body": "Wir freuen uns, dich begrüßen zu dürfen." 14 | } 15 | }, 16 | "de-AT": { 17 | "translation": { 18 | "title": "Servus!", 19 | "heading": "Willkommen bei {{appName}}!", 20 | "body": "Wir freuen uns, dich begrüßen zu dürfen." 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /public/custom_page.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | {{appName}} 9 | 10 | 11 | 12 |

{{appName}}

13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/de-AT/email_verification_link_expired.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | Email Verification 12 | 13 | 14 | 15 |

{{appName}}

16 |

Expired verification link!

17 |
18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /public/de-AT/email_verification_link_invalid.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | Email Verification 14 | 15 | 16 | 17 |

{{appName}}

18 |

Invalid verification link!

19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/de-AT/email_verification_send_fail.html: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | Email Verification 13 | 14 | 15 | 16 |

{{appName}}

17 |

Invalid link!

18 |

No link sent. User not found or email already verified.

19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/de-AT/email_verification_send_success.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | Email Verification 11 | 12 | 13 | 14 |

{{appName}}

15 |

Link sent!

16 |

A new link has been sent. Check your email.

17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/de-AT/email_verification_success.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | Email Verification 10 | 11 | 12 | 13 |

{{appName}}

14 |

Email verified!

15 |

Successfully verified your email for account: {{username}}.

16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/de-AT/password_reset_link_invalid.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | Password Reset 12 | 13 | 14 | 15 |

{{appName}}

16 |

Invalid password reset link!

17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/de-AT/password_reset_success.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | Password Reset 10 | 11 | 12 | 13 |

{{appName}}

14 |

Success!

15 |

Your password has been updated.

16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/de/email_verification_link_expired.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | Email Verification 12 | 13 | 14 | 15 |

{{appName}}

16 |

Expired verification link!

17 |
18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /public/de/email_verification_link_invalid.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | Email Verification 14 | 15 | 16 | 17 |

{{appName}}

18 |

Invalid verification link!

19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/de/email_verification_send_fail.html: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | Email Verification 13 | 14 | 15 | 16 |

{{appName}}

17 |

Invalid link!

18 |

No link sent. User not found or email already verified.

19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/de/email_verification_send_success.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | Email Verification 11 | 12 | 13 | 14 |

{{appName}}

15 |

Link sent!

16 |

A new link has been sent. Check your email.

17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/de/email_verification_success.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | Email Verification 10 | 11 | 12 | 13 |

{{appName}}

14 |

Email verified!

15 |

Successfully verified your email for account: {{username}}.

16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/de/password_reset_link_invalid.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | Password Reset 12 | 13 | 14 | 15 |

{{appName}}

16 |

Invalid password reset link!

17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/de/password_reset_success.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | Password Reset 10 | 11 | 12 | 13 |

{{appName}}

14 |

Success!

15 |

Your password has been updated.

16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/email_verification_link_expired.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | Email Verification 12 | 13 | 14 | 15 |

{{appName}}

16 |

Expired verification link!

17 |
18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /public/email_verification_link_invalid.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | Email Verification 14 | 15 | 16 | 17 |

{{appName}}

18 |

Invalid verification link!

19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/email_verification_send_fail.html: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | Email Verification 13 | 14 | 15 | 16 |

{{appName}}

17 |

Invalid link!

18 |

No link sent. User not found or email already verified.

19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/email_verification_send_success.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | Email Verification 11 | 12 | 13 | 14 |

{{appName}}

15 |

Link sent!

16 |

A new link has been sent. Check your email.

17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/email_verification_success.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | Email Verification 10 | 11 | 12 | 13 |

{{appName}}

14 |

Email verified!

15 |

Successfully verified your email for account: {{username}}.

16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/password_reset_link_invalid.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | Password Reset 12 | 13 | 14 | 15 |

{{appName}}

16 |

Invalid password reset link!

17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/password_reset_success.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | Password Reset 10 | 11 | 12 | 13 |

{{appName}}

14 |

Success!

15 |

Your password has been updated.

16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public_html/invalid_link.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | Invalid Link 10 | 38 | 39 | 40 | 41 |
42 |

Invalid Link

43 |
44 | 45 | 46 | -------------------------------------------------------------------------------- /public_html/link_send_fail.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | Invalid Link 10 | 38 | 39 | 40 | 41 |
42 |

No link sent. User not found or email already verified

43 |
44 | 45 | 46 | -------------------------------------------------------------------------------- /public_html/link_send_success.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | Invalid Link 10 | 38 | 39 | 40 | 41 |
42 |

Link Sent! Check your email.

43 |
44 | 45 | 46 | -------------------------------------------------------------------------------- /public_html/password_reset_success.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | Password Reset 10 | 24 | 25 |

Successfully updated your password!

26 | 27 | 28 | -------------------------------------------------------------------------------- /public_html/verify_email_success.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | Email Verification 10 | 24 | 25 |

Successfully verified your email!

26 | 27 | 28 | -------------------------------------------------------------------------------- /release_docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | set -x 3 | # GITHUB_ACTIONS=true SOURCE_TAG=test ./release_docs.sh 4 | 5 | if [ "${GITHUB_ACTIONS}" = "" ]; 6 | then 7 | echo "Cannot release docs without GITHUB_ACTIONS set" 8 | exit 0; 9 | fi 10 | if [ "${SOURCE_TAG}" = "" ]; 11 | then 12 | echo "Cannot release docs without SOURCE_TAG set" 13 | exit 0; 14 | fi 15 | REPO="https://github.com/parse-community/parse-server" 16 | 17 | rm -rf docs 18 | git clone -b gh-pages --single-branch $REPO ./docs 19 | cd docs 20 | git pull origin gh-pages 21 | cd .. 22 | 23 | RELEASE="release" 24 | VERSION="${SOURCE_TAG}" 25 | 26 | # change the default page to the latest 27 | echo "" > "docs/api/index.html" 28 | 29 | npm run definitions 30 | npm run docs 31 | 32 | mkdir -p "docs/api/${RELEASE}" 33 | cp -R out/* "docs/api/${RELEASE}" 34 | 35 | mkdir -p "docs/api/${VERSION}" 36 | cp -R out/* "docs/api/${VERSION}" 37 | 38 | # Copy other resources 39 | RESOURCE_DIR=".github" 40 | mkdir -p "docs/${RESOURCE_DIR}" 41 | cp "./.github/parse-server-logo.png" "docs/${RESOURCE_DIR}/" 42 | -------------------------------------------------------------------------------- /scripts/before_script_postgres.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo "[SCRIPT] Before Script :: Setup Parse DB for Postgres" 6 | 7 | PGPASSWORD=postgres psql -v ON_ERROR_STOP=1 -h localhost -U postgres <<-EOSQL 8 | CREATE DATABASE parse_server_postgres_adapter_test_database; 9 | \c parse_server_postgres_adapter_test_database; 10 | CREATE EXTENSION pgcrypto; 11 | CREATE EXTENSION postgis; 12 | CREATE EXTENSION postgis_topology; 13 | EOSQL 14 | -------------------------------------------------------------------------------- /scripts/before_script_postgres_conf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo "[SCRIPT] Before Script :: Setup Parse Postgres configuration file" 6 | 7 | # DB Version: 13 8 | # OS Type: linux 9 | # DB Type: web 10 | # Total Memory (RAM): 6 GB 11 | # CPUs num: 1 12 | # Data Storage: ssd 13 | 14 | PGPASSWORD=postgres psql -v ON_ERROR_STOP=1 -h localhost -U postgres <<-EOSQL 15 | ALTER SYSTEM SET max_connections TO '200'; 16 | ALTER SYSTEM SET shared_buffers TO '1536MB'; 17 | ALTER SYSTEM SET effective_cache_size TO '4608MB'; 18 | ALTER SYSTEM SET maintenance_work_mem TO '384MB'; 19 | ALTER SYSTEM SET checkpoint_completion_target TO '0.9'; 20 | ALTER SYSTEM SET wal_buffers TO '16MB'; 21 | ALTER SYSTEM SET default_statistics_target TO '100'; 22 | ALTER SYSTEM SET random_page_cost TO '1.1'; 23 | ALTER SYSTEM SET effective_io_concurrency TO '200'; 24 | ALTER SYSTEM SET work_mem TO '3932kB'; 25 | ALTER SYSTEM SET min_wal_size TO '1GB'; 26 | ALTER SYSTEM SET max_wal_size TO '4GB'; 27 | SELECT pg_reload_conf(); 28 | EOSQL 29 | 30 | exec "$@" 31 | -------------------------------------------------------------------------------- /spec/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@babel/plugin-proposal-object-rest-spread" 4 | ], 5 | "presets": [ 6 | "@babel/preset-typescript", 7 | ["@babel/preset-env", { 8 | "targets": { 9 | "node": "18" 10 | } 11 | }] 12 | ], 13 | "sourceMaps": "inline" 14 | } 15 | -------------------------------------------------------------------------------- /spec/Analytics.spec.js: -------------------------------------------------------------------------------- 1 | const analyticsAdapter = { 2 | appOpened: function () {}, 3 | trackEvent: function () {}, 4 | }; 5 | 6 | describe('AnalyticsController', () => { 7 | it('should track a simple event', done => { 8 | spyOn(analyticsAdapter, 'trackEvent').and.callThrough(); 9 | reconfigureServer({ 10 | analyticsAdapter, 11 | }) 12 | .then(() => { 13 | return Parse.Analytics.track('MyEvent', { 14 | key: 'value', 15 | count: '0', 16 | }); 17 | }) 18 | .then( 19 | () => { 20 | expect(analyticsAdapter.trackEvent).toHaveBeenCalled(); 21 | const lastCall = analyticsAdapter.trackEvent.calls.first(); 22 | const args = lastCall.args; 23 | expect(args[0]).toEqual('MyEvent'); 24 | expect(args[1]).toEqual({ 25 | dimensions: { 26 | key: 'value', 27 | count: '0', 28 | }, 29 | }); 30 | done(); 31 | }, 32 | err => { 33 | fail(JSON.stringify(err)); 34 | done(); 35 | } 36 | ); 37 | }); 38 | 39 | it('should track a app opened event', done => { 40 | spyOn(analyticsAdapter, 'appOpened').and.callThrough(); 41 | reconfigureServer({ 42 | analyticsAdapter, 43 | }) 44 | .then(() => { 45 | return Parse.Analytics.track('AppOpened', { 46 | key: 'value', 47 | count: '0', 48 | }); 49 | }) 50 | .then( 51 | () => { 52 | expect(analyticsAdapter.appOpened).toHaveBeenCalled(); 53 | const lastCall = analyticsAdapter.appOpened.calls.first(); 54 | const args = lastCall.args; 55 | expect(args[0]).toEqual({ 56 | dimensions: { 57 | key: 'value', 58 | count: '0', 59 | }, 60 | }); 61 | done(); 62 | }, 63 | err => { 64 | fail(JSON.stringify(err)); 65 | done(); 66 | } 67 | ); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /spec/ClientSDK.spec.js: -------------------------------------------------------------------------------- 1 | const ClientSDK = require('../lib/ClientSDK'); 2 | 3 | describe('ClientSDK', () => { 4 | it('should properly parse the SDK versions', () => { 5 | const clientSDKFromVersion = ClientSDK.fromString; 6 | expect(clientSDKFromVersion('i1.1.1')).toEqual({ 7 | sdk: 'i', 8 | version: '1.1.1', 9 | }); 10 | expect(clientSDKFromVersion('i1')).toEqual({ 11 | sdk: 'i', 12 | version: '1', 13 | }); 14 | expect(clientSDKFromVersion('apple-tv1.13.0')).toEqual({ 15 | sdk: 'apple-tv', 16 | version: '1.13.0', 17 | }); 18 | expect(clientSDKFromVersion('js1.9.0')).toEqual({ 19 | sdk: 'js', 20 | version: '1.9.0', 21 | }); 22 | }); 23 | 24 | it('should properly sastisfy', () => { 25 | expect( 26 | ClientSDK.compatible({ 27 | js: '>=1.9.0', 28 | })('js1.9.0') 29 | ).toBe(true); 30 | 31 | expect( 32 | ClientSDK.compatible({ 33 | js: '>=1.9.0', 34 | })('js2.0.0') 35 | ).toBe(true); 36 | 37 | expect( 38 | ClientSDK.compatible({ 39 | js: '>=1.9.0', 40 | })('js1.8.0') 41 | ).toBe(false); 42 | 43 | expect( 44 | ClientSDK.compatible({ 45 | js: '>=1.9.0', 46 | })(undefined) 47 | ).toBe(true); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /spec/Deprecator.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Deprecator = require('../lib/Deprecator/Deprecator'); 4 | 5 | describe('Deprecator', () => { 6 | let deprecations = []; 7 | 8 | beforeEach(async () => { 9 | deprecations = [{ optionKey: 'exampleKey', changeNewDefault: 'exampleNewDefault' }]; 10 | }); 11 | 12 | it('deprecations are an array', async () => { 13 | expect(Deprecator._getDeprecations()).toBeInstanceOf(Array); 14 | }); 15 | 16 | it('logs deprecation for new default', async () => { 17 | deprecations = [{ optionKey: 'exampleKey', changeNewDefault: 'exampleNewDefault' }]; 18 | 19 | spyOn(Deprecator, '_getDeprecations').and.callFake(() => deprecations); 20 | const logger = require('../lib/logger').logger; 21 | const logSpy = spyOn(logger, 'warn').and.callFake(() => {}); 22 | 23 | await reconfigureServer(); 24 | expect(logSpy.calls.all()[0].args[0]).toEqual( 25 | `DeprecationWarning: The Parse Server option '${deprecations[0].optionKey}' default will change to '${deprecations[0].changeNewDefault}' in a future version.` 26 | ); 27 | }); 28 | 29 | it('does not log deprecation for new default if option is set manually', async () => { 30 | deprecations = [{ optionKey: 'exampleKey', changeNewDefault: 'exampleNewDefault' }]; 31 | 32 | spyOn(Deprecator, '_getDeprecations').and.callFake(() => deprecations); 33 | const logSpy = spyOn(Deprecator, '_logOption').and.callFake(() => {}); 34 | await reconfigureServer({ [deprecations[0].optionKey]: 'manuallySet' }); 35 | expect(logSpy).not.toHaveBeenCalled(); 36 | }); 37 | 38 | it('logs runtime deprecation', async () => { 39 | const logger = require('../lib/logger').logger; 40 | const logSpy = spyOn(logger, 'warn').and.callFake(() => {}); 41 | const options = { usage: 'Doing this', solution: 'Do that instead.' }; 42 | 43 | Deprecator.logRuntimeDeprecation(options); 44 | expect(logSpy.calls.all()[0].args[0]).toEqual( 45 | `DeprecationWarning: ${options.usage} is deprecated and will be removed in a future version. ${options.solution}` 46 | ); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /spec/EnableExpressErrorHandler.spec.js: -------------------------------------------------------------------------------- 1 | const request = require('../lib/request'); 2 | 3 | describe('Enable express error handler', () => { 4 | it('should call the default handler in case of error, like updating a non existing object', async done => { 5 | spyOn(console, 'error'); 6 | const parseServer = await reconfigureServer({ 7 | enableExpressErrorHandler: true, 8 | }); 9 | parseServer.app.use(function (err, req, res, next) { 10 | expect(err.message).toBe('Object not found.'); 11 | next(err); 12 | }); 13 | 14 | try { 15 | await request({ 16 | method: 'PUT', 17 | url: defaultConfiguration.serverURL + '/classes/AnyClass/nonExistingId', 18 | headers: { 19 | 'X-Parse-Application-Id': defaultConfiguration.appId, 20 | 'X-Parse-Master-Key': defaultConfiguration.masterKey, 21 | 'Content-Type': 'application/json', 22 | }, 23 | body: { someField: 'blablabla' }, 24 | }); 25 | fail('Should throw error'); 26 | } catch (response) { 27 | expect(response).toBeDefined(); 28 | expect(response.status).toEqual(500); 29 | parseServer.server.close(done); 30 | } 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /spec/EventEmitterPubSub.spec.js: -------------------------------------------------------------------------------- 1 | const EventEmitterPubSub = require('../lib/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub; 2 | 3 | describe('EventEmitterPubSub', function () { 4 | it('can publish and subscribe', function () { 5 | const publisher = EventEmitterPubSub.createPublisher(); 6 | const subscriber = EventEmitterPubSub.createSubscriber(); 7 | subscriber.subscribe('testChannel'); 8 | // Register mock checked for subscriber 9 | let isChecked = false; 10 | subscriber.on('message', function (channel, message) { 11 | isChecked = true; 12 | expect(channel).toBe('testChannel'); 13 | expect(message).toBe('testMessage'); 14 | }); 15 | 16 | publisher.publish('testChannel', 'testMessage'); 17 | // Make sure the callback is checked 18 | expect(isChecked).toBe(true); 19 | }); 20 | 21 | it('can unsubscribe', function () { 22 | const publisher = EventEmitterPubSub.createPublisher(); 23 | const subscriber = EventEmitterPubSub.createSubscriber(); 24 | subscriber.subscribe('testChannel'); 25 | subscriber.unsubscribe('testChannel'); 26 | // Register mock checked for subscriber 27 | let isCalled = false; 28 | subscriber.on('message', function () { 29 | isCalled = true; 30 | }); 31 | 32 | publisher.publish('testChannel', 'testMessage'); 33 | // Make sure the callback is not called 34 | expect(isCalled).toBe(false); 35 | }); 36 | 37 | it('can unsubscribe not subscribing channel', function () { 38 | const subscriber = EventEmitterPubSub.createSubscriber(); 39 | 40 | // Make sure subscriber does not throw exception 41 | subscriber.unsubscribe('testChannel'); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /spec/InMemoryCache.spec.js: -------------------------------------------------------------------------------- 1 | const InMemoryCache = require('../lib/Adapters/Cache/InMemoryCache').default; 2 | 3 | describe('InMemoryCache', function () { 4 | const BASE_TTL = { 5 | ttl: 100, 6 | }; 7 | const NO_EXPIRE_TTL = { 8 | ttl: NaN, 9 | }; 10 | const KEY = 'hello'; 11 | const KEY_2 = KEY + '_2'; 12 | 13 | const VALUE = 'world'; 14 | 15 | function wait(sleep) { 16 | return new Promise(function (resolve) { 17 | setTimeout(resolve, sleep); 18 | }); 19 | } 20 | 21 | it('should destroy a expire items in the cache', done => { 22 | const cache = new InMemoryCache(BASE_TTL); 23 | 24 | cache.put(KEY, VALUE); 25 | 26 | let value = cache.get(KEY); 27 | expect(value).toEqual(VALUE); 28 | 29 | wait(BASE_TTL.ttl * 10).then(() => { 30 | value = cache.get(KEY); 31 | expect(value).toEqual(null); 32 | done(); 33 | }); 34 | }); 35 | 36 | it('should delete items', done => { 37 | const cache = new InMemoryCache(NO_EXPIRE_TTL); 38 | cache.put(KEY, VALUE); 39 | cache.put(KEY_2, VALUE); 40 | expect(cache.get(KEY)).toEqual(VALUE); 41 | expect(cache.get(KEY_2)).toEqual(VALUE); 42 | 43 | cache.del(KEY); 44 | expect(cache.get(KEY)).toEqual(null); 45 | expect(cache.get(KEY_2)).toEqual(VALUE); 46 | 47 | cache.del(KEY_2); 48 | expect(cache.get(KEY)).toEqual(null); 49 | expect(cache.get(KEY_2)).toEqual(null); 50 | done(); 51 | }); 52 | 53 | it('should clear all items', done => { 54 | const cache = new InMemoryCache(NO_EXPIRE_TTL); 55 | cache.put(KEY, VALUE); 56 | cache.put(KEY_2, VALUE); 57 | 58 | expect(cache.get(KEY)).toEqual(VALUE); 59 | expect(cache.get(KEY_2)).toEqual(VALUE); 60 | cache.clear(); 61 | 62 | expect(cache.get(KEY)).toEqual(null); 63 | expect(cache.get(KEY_2)).toEqual(null); 64 | done(); 65 | }); 66 | 67 | it('should deafult TTL to 5 seconds', () => { 68 | const cache = new InMemoryCache({}); 69 | expect(cache.ttl).toEqual(5 * 1000); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /spec/InMemoryCacheAdapter.spec.js: -------------------------------------------------------------------------------- 1 | const InMemoryCacheAdapter = require('../lib/Adapters/Cache/InMemoryCacheAdapter').default; 2 | 3 | describe('InMemoryCacheAdapter', function () { 4 | const KEY = 'hello'; 5 | const VALUE = 'world'; 6 | 7 | function wait(sleep) { 8 | return new Promise(function (resolve) { 9 | setTimeout(resolve, sleep); 10 | }); 11 | } 12 | 13 | it('should expose promisifyed methods', done => { 14 | const cache = new InMemoryCacheAdapter({ 15 | ttl: NaN, 16 | }); 17 | 18 | // Verify all methods return promises. 19 | Promise.all([cache.put(KEY, VALUE), cache.del(KEY), cache.get(KEY), cache.clear()]).then(() => { 20 | done(); 21 | }); 22 | }); 23 | 24 | it('should get/set/clear', done => { 25 | const cache = new InMemoryCacheAdapter({ 26 | ttl: NaN, 27 | }); 28 | 29 | cache 30 | .put(KEY, VALUE) 31 | .then(() => cache.get(KEY)) 32 | .then(value => expect(value).toEqual(VALUE)) 33 | .then(() => cache.clear()) 34 | .then(() => cache.get(KEY)) 35 | .then(value => expect(value).toEqual(null)) 36 | .then(done); 37 | }); 38 | 39 | it('should expire after ttl', done => { 40 | const cache = new InMemoryCacheAdapter({ 41 | ttl: 10, 42 | }); 43 | 44 | cache 45 | .put(KEY, VALUE) 46 | .then(() => cache.get(KEY)) 47 | .then(value => expect(value).toEqual(VALUE)) 48 | .then(wait.bind(null, 50)) 49 | .then(() => cache.get(KEY)) 50 | .then(value => expect(value).toEqual(null)) 51 | .then(done); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /spec/NullCacheAdapter.spec.js: -------------------------------------------------------------------------------- 1 | const NullCacheAdapter = require('../lib/Adapters/Cache/NullCacheAdapter').default; 2 | 3 | describe('NullCacheAdapter', function () { 4 | const KEY = 'hello'; 5 | const VALUE = 'world'; 6 | 7 | it('should expose promisifyed methods', done => { 8 | const cache = new NullCacheAdapter({ 9 | ttl: NaN, 10 | }); 11 | 12 | // Verify all methods return promises. 13 | Promise.all([cache.put(KEY, VALUE), cache.del(KEY), cache.get(KEY), cache.clear()]).then(() => { 14 | done(); 15 | }); 16 | }); 17 | 18 | it('should get/set/clear', done => { 19 | const cache = new NullCacheAdapter({ 20 | ttl: NaN, 21 | }); 22 | 23 | cache 24 | .put(KEY, VALUE) 25 | .then(() => cache.get(KEY)) 26 | .then(value => expect(value).toEqual(null)) 27 | .then(() => cache.clear()) 28 | .then(() => cache.get(KEY)) 29 | .then(value => expect(value).toEqual(null)) 30 | .then(done); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /spec/ParseGraphQLClassNameTransformer.spec.js: -------------------------------------------------------------------------------- 1 | const { transformClassNameToGraphQL } = require('../lib/GraphQL/transformers/className'); 2 | 3 | describe('transformClassNameToGraphQL', () => { 4 | it('should remove starting _ and tansform first letter to upper case', () => { 5 | expect(['_User', '_user', 'User', 'user'].map(transformClassNameToGraphQL)).toEqual([ 6 | 'User', 7 | 'User', 8 | 'User', 9 | 'User', 10 | ]); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /spec/ParseLiveQueryRedis.spec.js: -------------------------------------------------------------------------------- 1 | if (process.env.PARSE_SERVER_TEST_CACHE === 'redis') { 2 | describe('ParseLiveQuery redis', () => { 3 | afterEach(async () => { 4 | const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); 5 | client.close(); 6 | }); 7 | it('can connect', async () => { 8 | await reconfigureServer({ 9 | appId: 'redis_live_query', 10 | startLiveQueryServer: true, 11 | liveQuery: { 12 | classNames: ['TestObject'], 13 | redisURL: 'redis://localhost:6379', 14 | }, 15 | liveQueryServerOptions: { 16 | redisURL: 'redis://localhost:6379', 17 | }, 18 | }); 19 | const subscription = await new Parse.Query('TestObject').subscribe(); 20 | const [object] = await Promise.all([ 21 | new Parse.Object('TestObject').save(), 22 | new Promise(resolve => 23 | subscription.on('create', () => { 24 | resolve(); 25 | }) 26 | ), 27 | ]); 28 | await Promise.all([ 29 | new Promise(resolve => 30 | subscription.on('delete', () => { 31 | resolve(); 32 | }) 33 | ), 34 | object.destroy(), 35 | ]); 36 | }); 37 | 38 | it('can call connect twice', async () => { 39 | const server = await reconfigureServer({ 40 | appId: 'redis_live_query', 41 | startLiveQueryServer: true, 42 | liveQuery: { 43 | classNames: ['TestObject'], 44 | redisURL: 'redis://localhost:6379', 45 | }, 46 | liveQueryServerOptions: { 47 | redisURL: 'redis://localhost:6379', 48 | }, 49 | }); 50 | expect(server.config.liveQueryController.liveQueryPublisher.parsePublisher.isOpen).toBeTrue(); 51 | await server.config.liveQueryController.connect(); 52 | expect(server.config.liveQueryController.liveQueryPublisher.parsePublisher.isOpen).toBeTrue(); 53 | expect(server.liveQueryServer.subscriber.isOpen).toBe(true); 54 | await server.liveQueryServer.connect(); 55 | expect(server.liveQueryServer.subscriber.isOpen).toBe(true); 56 | }); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /spec/ParseServer.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* Tests for ParseServer.js */ 3 | const express = require('express'); 4 | const ParseServer = require('../lib/ParseServer').default; 5 | const path = require('path'); 6 | const { spawn } = require('child_process'); 7 | 8 | describe('Server Url Checks', () => { 9 | let server; 10 | beforeEach(done => { 11 | if (!server) { 12 | const app = express(); 13 | app.get('/health', function (req, res) { 14 | res.json({ 15 | status: 'ok', 16 | }); 17 | }); 18 | server = app.listen(13376, undefined, done); 19 | } else { 20 | done(); 21 | } 22 | }); 23 | 24 | afterAll(done => { 25 | Parse.serverURL = 'http://localhost:8378/1'; 26 | server.close(done); 27 | }); 28 | 29 | it('validate good server url', async () => { 30 | Parse.serverURL = 'http://localhost:13376'; 31 | const response = await ParseServer.verifyServerUrl(); 32 | expect(response).toBeTrue(); 33 | }); 34 | 35 | it('mark bad server url', async () => { 36 | spyOn(console, 'warn').and.callFake(() => {}); 37 | Parse.serverURL = 'notavalidurl'; 38 | const response = await ParseServer.verifyServerUrl(); 39 | expect(response).not.toBeTrue(); 40 | expect(console.warn).toHaveBeenCalledWith( 41 | `\nWARNING, Unable to connect to 'notavalidurl' as the URL is invalid. Cloud code and push notifications may be unavailable!\n` 42 | ); 43 | }); 44 | 45 | it('does not have unhandled promise rejection in the case of load error', done => { 46 | const parseServerProcess = spawn(path.resolve(__dirname, './support/FailingServer.js')); 47 | let stdout; 48 | let stderr; 49 | parseServerProcess.stdout.on('data', data => { 50 | stdout = data.toString(); 51 | }); 52 | parseServerProcess.stderr.on('data', data => { 53 | stderr = data.toString(); 54 | }); 55 | parseServerProcess.on('close', async code => { 56 | expect(code).toEqual(1); 57 | expect(stdout).not.toContain('UnhandledPromiseRejectionWarning'); 58 | expect(stderr).toContain('MongoServerSelectionError'); 59 | await reconfigureServer(); 60 | done(); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /spec/ParseWebSocket.spec.js: -------------------------------------------------------------------------------- 1 | const ParseWebSocket = require('../lib/LiveQuery/ParseWebSocketServer').ParseWebSocket; 2 | 3 | describe('ParseWebSocket', function () { 4 | it('can be initialized', function () { 5 | const ws = {}; 6 | const parseWebSocket = new ParseWebSocket(ws); 7 | 8 | expect(parseWebSocket.ws).toBe(ws); 9 | }); 10 | 11 | it('can handle disconnect event', function (done) { 12 | const ws = { 13 | onclose: () => {}, 14 | }; 15 | const parseWebSocket = new ParseWebSocket(ws); 16 | parseWebSocket.on('disconnect', () => { 17 | done(); 18 | }); 19 | ws.onclose(); 20 | }); 21 | 22 | it('can handle message event', function (done) { 23 | const ws = { 24 | onmessage: () => {}, 25 | }; 26 | const parseWebSocket = new ParseWebSocket(ws); 27 | parseWebSocket.on('message', () => { 28 | done(); 29 | }); 30 | ws.onmessage(); 31 | }); 32 | 33 | it('can send a message', function () { 34 | const ws = { 35 | send: jasmine.createSpy('send'), 36 | }; 37 | const parseWebSocket = new ParseWebSocket(ws); 38 | parseWebSocket.send('message'); 39 | 40 | expect(parseWebSocket.ws.send).toHaveBeenCalledWith('message'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /spec/PromiseRouter.spec.js: -------------------------------------------------------------------------------- 1 | const PromiseRouter = require('../lib/PromiseRouter').default; 2 | 3 | describe('PromiseRouter', () => { 4 | it('should properly handle rejects', done => { 5 | const router = new PromiseRouter(); 6 | router.route( 7 | 'GET', 8 | '/dummy', 9 | () => { 10 | return Promise.reject({ 11 | error: 'an error', 12 | code: -1, 13 | }); 14 | }, 15 | () => { 16 | fail('this should not be called'); 17 | } 18 | ); 19 | 20 | router.routes[0].handler({}).then( 21 | result => { 22 | jfail(result); 23 | fail('this should not be called'); 24 | done(); 25 | }, 26 | error => { 27 | expect(error.error).toEqual('an error'); 28 | expect(error.code).toEqual(-1); 29 | done(); 30 | } 31 | ); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /spec/PushQueue.spec.js: -------------------------------------------------------------------------------- 1 | const Config = require('../lib/Config'); 2 | const { PushQueue } = require('../lib/Push/PushQueue'); 3 | 4 | describe('PushQueue', () => { 5 | describe('With a defined channel', () => { 6 | it('should be propagated to the PushWorker and PushQueue', done => { 7 | reconfigureServer({ 8 | push: { 9 | queueOptions: { 10 | disablePushWorker: false, 11 | channel: 'my-specific-channel', 12 | }, 13 | adapter: { 14 | send() { 15 | return Promise.resolve(); 16 | }, 17 | getValidPushTypes() { 18 | return []; 19 | }, 20 | }, 21 | }, 22 | }) 23 | .then(() => { 24 | const config = Config.get(Parse.applicationId); 25 | expect(config.pushWorker.channel).toEqual('my-specific-channel', 'pushWorker.channel'); 26 | expect(config.pushControllerQueue.channel).toEqual( 27 | 'my-specific-channel', 28 | 'pushWorker.channel' 29 | ); 30 | }) 31 | .then(done, done.fail); 32 | }); 33 | }); 34 | 35 | describe('Default channel', () => { 36 | it('should be prefixed with the applicationId', done => { 37 | reconfigureServer({ 38 | push: { 39 | queueOptions: { 40 | disablePushWorker: false, 41 | }, 42 | adapter: { 43 | send() { 44 | return Promise.resolve(); 45 | }, 46 | getValidPushTypes() { 47 | return []; 48 | }, 49 | }, 50 | }, 51 | }) 52 | .then(() => { 53 | const config = Config.get(Parse.applicationId); 54 | expect(PushQueue.defaultPushChannel()).toEqual('test-parse-server-push'); 55 | expect(config.pushWorker.channel).toEqual('test-parse-server-push'); 56 | expect(config.pushControllerQueue.channel).toEqual('test-parse-server-push'); 57 | }) 58 | .then(done, done.fail); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /spec/RedisPubSub.spec.js: -------------------------------------------------------------------------------- 1 | const RedisPubSub = require('../lib/Adapters/PubSub/RedisPubSub').RedisPubSub; 2 | 3 | describe('RedisPubSub', function () { 4 | beforeEach(function (done) { 5 | // Mock redis 6 | const createClient = jasmine.createSpy('createClient').and.returnValue({ 7 | connect: jasmine.createSpy('connect').and.resolveTo(), 8 | on: jasmine.createSpy('on'), 9 | }); 10 | jasmine.mockLibrary('redis', 'createClient', createClient); 11 | done(); 12 | }); 13 | 14 | it('can create publisher', function () { 15 | RedisPubSub.createPublisher({ 16 | redisURL: 'redisAddress', 17 | redisOptions: { socket_keepalive: true }, 18 | }); 19 | 20 | const redis = require('redis'); 21 | expect(redis.createClient).toHaveBeenCalledWith({ 22 | url: 'redisAddress', 23 | socket_keepalive: true, 24 | no_ready_check: true, 25 | }); 26 | }); 27 | 28 | it('can create subscriber', function () { 29 | RedisPubSub.createSubscriber({ 30 | redisURL: 'redisAddress', 31 | redisOptions: { socket_keepalive: true }, 32 | }); 33 | 34 | const redis = require('redis'); 35 | expect(redis.createClient).toHaveBeenCalledWith({ 36 | url: 'redisAddress', 37 | socket_keepalive: true, 38 | no_ready_check: true, 39 | }); 40 | }); 41 | 42 | afterEach(function () { 43 | jasmine.restoreLibrary('redis', 'createClient'); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /spec/SessionTokenCache.spec.js: -------------------------------------------------------------------------------- 1 | const SessionTokenCache = require('../lib/LiveQuery/SessionTokenCache').SessionTokenCache; 2 | 3 | describe('SessionTokenCache', function () { 4 | beforeEach(function (done) { 5 | const Parse = require('parse/node'); 6 | 7 | spyOn(Parse, 'Query').and.returnValue({ 8 | first: jasmine.createSpy('first').and.returnValue( 9 | Promise.resolve( 10 | new Parse.Object('_Session', { 11 | user: new Parse.User({ id: 'userId' }), 12 | }) 13 | ) 14 | ), 15 | equalTo: function () {}, 16 | }); 17 | 18 | done(); 19 | }); 20 | 21 | it('can get undefined userId', function (done) { 22 | const sessionTokenCache = new SessionTokenCache(); 23 | 24 | sessionTokenCache.getUserId(undefined).then( 25 | () => {}, 26 | error => { 27 | expect(error).not.toBeNull(); 28 | done(); 29 | } 30 | ); 31 | }); 32 | 33 | it('can get existing userId', function (done) { 34 | const sessionTokenCache = new SessionTokenCache(); 35 | const sessionToken = 'sessionToken'; 36 | const userId = 'userId'; 37 | sessionTokenCache.cache.set(sessionToken, userId); 38 | 39 | sessionTokenCache.getUserId(sessionToken).then(userIdFromCache => { 40 | expect(userIdFromCache).toBe(userId); 41 | done(); 42 | }); 43 | }); 44 | 45 | it('can get new userId', function (done) { 46 | const sessionTokenCache = new SessionTokenCache(); 47 | 48 | sessionTokenCache.getUserId('sessionToken').then(userIdFromCache => { 49 | expect(userIdFromCache).toBe('userId'); 50 | expect(sessionTokenCache.cache.size).toBe(1); 51 | done(); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /spec/Utils.spec.js: -------------------------------------------------------------------------------- 1 | const Utils = require('../src/Utils'); 2 | 3 | describe('Utils', () => { 4 | describe('encodeForUrl', () => { 5 | it('should properly escape email with all special ASCII characters for use in URLs', async () => { 6 | const values = [ 7 | { input: `!\"'),.:;<>?]^}`, output: '%21%22%27%29%2C%2E%3A%3B%3C%3E%3F%5D%5E%7D' }, 8 | ] 9 | for (const value of values) { 10 | expect(Utils.encodeForUrl(value.input)).toBe(value.output); 11 | } 12 | }); 13 | }); 14 | 15 | describe('addNestedKeysToRoot', () => { 16 | it('should move the nested keys to root of object', async () => { 17 | const obj = { 18 | a: 1, 19 | b: { 20 | c: 2, 21 | d: 3 22 | }, 23 | e: 4 24 | }; 25 | Utils.addNestedKeysToRoot(obj, 'b'); 26 | expect(obj).toEqual({ 27 | a: 1, 28 | c: 2, 29 | d: 3, 30 | e: 4 31 | }); 32 | }); 33 | 34 | it('should not modify the object if the key does not exist', async () => { 35 | const obj = { 36 | a: 1, 37 | e: 4 38 | }; 39 | Utils.addNestedKeysToRoot(obj, 'b'); 40 | expect(obj).toEqual({ 41 | a: 1, 42 | e: 4 43 | }); 44 | }); 45 | 46 | it('should not modify the object if the key is not an object', () => { 47 | const obj = { 48 | a: 1, 49 | b: 2, 50 | e: 4 51 | }; 52 | Utils.addNestedKeysToRoot(obj, 'b'); 53 | expect(obj).toEqual({ 54 | a: 1, 55 | b: 2, 56 | e: 4 57 | }); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /spec/cloud/cloudCodeAbsoluteFile.js: -------------------------------------------------------------------------------- 1 | Parse.Cloud.define('cloudCodeInFile', () => { 2 | return 'It is possible to define cloud code in a file.'; 3 | }); 4 | -------------------------------------------------------------------------------- /spec/cloud/cloudCodeModuleFile.js: -------------------------------------------------------------------------------- 1 | Parse.Cloud.define('cloudCodeInFile', () => { 2 | return 'It is possible to define cloud code in a file.'; 3 | }); 4 | -------------------------------------------------------------------------------- /spec/cloud/cloudCodeRelativeFile.js: -------------------------------------------------------------------------------- 1 | Parse.Cloud.define('cloudCodeInFile', () => { 2 | return 'It is possible to define cloud code in a file.'; 3 | }); 4 | -------------------------------------------------------------------------------- /spec/configs/CLIConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "arg1": "my_app", 3 | "arg2": "8888", 4 | "arg3": "hello", 5 | "arg4": "/1" 6 | } 7 | -------------------------------------------------------------------------------- /spec/configs/CLIConfigApps.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "arg1": "my_app", 5 | "arg2": 8888, 6 | "arg3": "hello", 7 | "arg4": "/1" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /spec/configs/CLIConfigAuth.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": "test", 3 | "appId": "test", 4 | "masterKey": "test", 5 | "logLevel": "error", 6 | "auth": { 7 | "facebook": { 8 | "appIds": "test" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /spec/configs/CLIConfigFail.json: -------------------------------------------------------------------------------- 1 | { 2 | "arg1": "my_app", 3 | "arg2": "hello", 4 | "arg3": "hello", 5 | "arg4": "/1" 6 | } 7 | -------------------------------------------------------------------------------- /spec/configs/CLIConfigFailTooManyApps.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "arg1": "my_app", 5 | "arg2": "99999", 6 | "arg3": "hello", 7 | "arg4": "/1" 8 | }, 9 | { 10 | "arg1": "my_app2", 11 | "arg2": "9999", 12 | "arg3": "hello", 13 | "arg4": "/1" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /spec/configs/CLIConfigUnknownArg.json: -------------------------------------------------------------------------------- 1 | { 2 | "arg1": "my_app", 3 | "arg2": "8888", 4 | "arg3": "hello", 5 | "myArg": "/1" 6 | } 7 | -------------------------------------------------------------------------------- /spec/dependencies/mock-files-adapter/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A mock files adapter for testing. 3 | */ 4 | class MockFilesAdapter { 5 | constructor(options = {}) { 6 | if (options.throw) { 7 | throw 'MockFilesAdapterConstructor'; 8 | } 9 | } 10 | createFile() { 11 | return 'MockFilesAdapterCreateFile'; 12 | } 13 | deleteFile() { 14 | return 'MockFilesAdapterDeleteFile'; 15 | } 16 | getFileData() { 17 | return 'MockFilesAdapterGetFileData'; 18 | } 19 | getFileLocation() { 20 | return 'MockFilesAdapterGetFileLocation'; 21 | } 22 | validateFilename() { 23 | return 'MockFilesAdapterValidateFilename'; 24 | } 25 | handleFileStream() { 26 | return 'MockFilesAdapterHandleFileStream'; 27 | } 28 | } 29 | 30 | module.exports = MockFilesAdapter; 31 | module.exports.default = MockFilesAdapter; 32 | -------------------------------------------------------------------------------- /spec/dependencies/mock-files-adapter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mock-files-adapter", 3 | "version": "1.0.0", 4 | "description": "Mock files adapter for tests.", 5 | "main": "index.js" 6 | } 7 | -------------------------------------------------------------------------------- /spec/dependencies/mock-mail-adapter/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A mock mail adapter for testing. 3 | */ 4 | class MockMailAdapter { 5 | constructor(options = {}) { 6 | if (options.throw) { 7 | throw 'MockMailAdapterConstructor'; 8 | } 9 | } 10 | sendMail() { 11 | return 'MockMailAdapterSendMail'; 12 | } 13 | } 14 | 15 | module.exports = MockMailAdapter; 16 | -------------------------------------------------------------------------------- /spec/dependencies/mock-mail-adapter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mock-mail-adapter", 3 | "version": "1.0.0", 4 | "description": "Mock mail adapter for tests.", 5 | "main": "index.js" 6 | } 7 | -------------------------------------------------------------------------------- /spec/eslint.config.js: -------------------------------------------------------------------------------- 1 | const js = require("@eslint/js"); 2 | const globals = require("globals"); 3 | module.exports = [ 4 | js.configs.recommended, 5 | { 6 | languageOptions: { 7 | ecmaVersion: "latest", 8 | sourceType: "module", 9 | globals: { 10 | ...globals.node, 11 | ...globals.jasmine, 12 | mockFetch: "readonly", 13 | Parse: "readonly", 14 | reconfigureServer: "readonly", 15 | createTestUser: "readonly", 16 | jfail: "readonly", 17 | ok: "readonly", 18 | strictEqual: "readonly", 19 | TestObject: "readonly", 20 | Item: "readonly", 21 | Container: "readonly", 22 | equal: "readonly", 23 | expectAsync: "readonly", 24 | notEqual: "readonly", 25 | it_id: "readonly", 26 | fit_id: "readonly", 27 | it_only_db: "readonly", 28 | it_only_mongodb_version: "readonly", 29 | it_only_postgres_version: "readonly", 30 | it_only_node_version: "readonly", 31 | fit_only_mongodb_version: "readonly", 32 | fit_only_postgres_version: "readonly", 33 | fit_only_node_version: "readonly", 34 | it_exclude_dbs: "readonly", 35 | fit_exclude_dbs: "readonly", 36 | describe_only_db: "readonly", 37 | fdescribe_only_db: "readonly", 38 | describe_only: "readonly", 39 | fdescribe_only: "readonly", 40 | on_db: "readonly", 41 | defaultConfiguration: "readonly", 42 | range: "readonly", 43 | jequal: "readonly", 44 | create: "readonly", 45 | arrayContains: "readonly", 46 | databaseAdapter: "readonly", 47 | databaseURI: "readonly" 48 | }, 49 | }, 50 | rules: { 51 | "no-console": "off", 52 | "no-var": "error", 53 | "no-unused-vars": "off", 54 | "no-useless-escape": "off", 55 | } 56 | }, 57 | ]; 58 | -------------------------------------------------------------------------------- /spec/features.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const request = require('../lib/request'); 4 | 5 | describe('features', () => { 6 | it('should return the serverInfo', async () => { 7 | const response = await request({ 8 | url: 'http://localhost:8378/1/serverInfo', 9 | json: true, 10 | headers: { 11 | 'X-Parse-Application-Id': 'test', 12 | 'X-Parse-REST-API-Key': 'rest', 13 | 'X-Parse-Master-Key': 'test', 14 | }, 15 | }); 16 | const data = response.data; 17 | expect(data).toBeDefined(); 18 | expect(data.features).toBeDefined(); 19 | expect(data.parseServerVersion).toBeDefined(); 20 | }); 21 | 22 | it('requires the master key to get features', async done => { 23 | try { 24 | await request({ 25 | url: 'http://localhost:8378/1/serverInfo', 26 | json: true, 27 | headers: { 28 | 'X-Parse-Application-Id': 'test', 29 | 'X-Parse-REST-API-Key': 'rest', 30 | }, 31 | }); 32 | done.fail('The serverInfo request should be rejected without the master key'); 33 | } catch (error) { 34 | expect(error.status).toEqual(403); 35 | expect(error.data.error).toEqual('unauthorized: master key is required'); 36 | done(); 37 | } 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /spec/support/CustomAuth.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | validateAppId: function () { 3 | return Promise.resolve(); 4 | }, 5 | validateAuthData: function (authData) { 6 | if (authData.token == 'my-token') { 7 | return Promise.resolve(); 8 | } 9 | return Promise.reject(); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /spec/support/CustomAuthFunction.js: -------------------------------------------------------------------------------- 1 | module.exports = function (validAuthData) { 2 | return { 3 | validateAppId: function () { 4 | return Promise.resolve(); 5 | }, 6 | validateAuthData: function (authData) { 7 | if (authData.token == validAuthData.token) { 8 | return Promise.resolve(); 9 | } 10 | return Promise.reject(); 11 | }, 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /spec/support/CustomMiddleware.js: -------------------------------------------------------------------------------- 1 | module.exports = function (req, res, next) { 2 | res.set('X-Yolo', '1'); 3 | next(); 4 | }; 5 | -------------------------------------------------------------------------------- /spec/support/FailingServer.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const MongoStorageAdapter = require('../../lib/Adapters/Storage/Mongo/MongoStorageAdapter').default; 3 | const { GridFSBucketAdapter } = require('../../lib/Adapters/Files/GridFSBucketAdapter'); 4 | 5 | const ParseServer = require('../../lib/index').ParseServer; 6 | 7 | const databaseURI = 'mongodb://doesnotexist:27017/parseServerMongoAdapterTestDatabase'; 8 | 9 | (async () => { 10 | try { 11 | await ParseServer.startApp({ 12 | appId: 'test', 13 | masterKey: 'test', 14 | databaseAdapter: new MongoStorageAdapter({ 15 | uri: databaseURI, 16 | mongoOptions: { 17 | serverSelectionTimeoutMS: 2000, 18 | }, 19 | }), 20 | filesAdapter: new GridFSBucketAdapter(databaseURI), 21 | }); 22 | } catch (e) { 23 | process.exit(1); 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /spec/support/MockAdapter.js: -------------------------------------------------------------------------------- 1 | module.exports = function (options) { 2 | return { 3 | options: options, 4 | }; 5 | }; 6 | -------------------------------------------------------------------------------- /spec/support/MockDatabaseAdapter.js: -------------------------------------------------------------------------------- 1 | module.exports = function (options) { 2 | return { 3 | options: options, 4 | send: function () {}, 5 | getDatabaseURI: function () { 6 | return options.databaseURI; 7 | }, 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /spec/support/MockEmailAdapter.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sendVerificationEmail: () => Promise.resolve(), 3 | sendPasswordResetEmail: () => Promise.resolve(), 4 | sendMail: () => Promise.resolve(), 5 | }; 6 | -------------------------------------------------------------------------------- /spec/support/MockEmailAdapterWithOptions.js: -------------------------------------------------------------------------------- 1 | module.exports = options => { 2 | if (!options) { 3 | throw 'Options were not provided'; 4 | } 5 | const adapter = { 6 | sendVerificationEmail: () => Promise.resolve(), 7 | sendPasswordResetEmail: () => Promise.resolve(), 8 | sendMail: () => Promise.resolve(), 9 | }; 10 | if (options.sendMail) { 11 | adapter.sendMail = options.sendMail; 12 | } 13 | if (options.sendPasswordResetEmail) { 14 | adapter.sendPasswordResetEmail = options.sendPasswordResetEmail; 15 | } 16 | if (options.sendVerificationEmail) { 17 | adapter.sendVerificationEmail = options.sendVerificationEmail; 18 | } 19 | 20 | return adapter; 21 | }; 22 | -------------------------------------------------------------------------------- /spec/support/MockLdapServer.js: -------------------------------------------------------------------------------- 1 | const ldapjs = require('ldapjs'); 2 | const fs = require('fs'); 3 | 4 | const tlsOptions = { 5 | key: fs.readFileSync(__dirname + '/cert/key.pem'), 6 | certificate: fs.readFileSync(__dirname + '/cert/cert.pem'), 7 | }; 8 | 9 | function newServer(port, dn, provokeSearchError = false, ssl = false) { 10 | const server = ssl ? ldapjs.createServer(tlsOptions) : ldapjs.createServer(); 11 | 12 | server.bind('o=example', function (req, res, next) { 13 | if (req.dn.toString() !== dn || req.credentials !== 'secret') 14 | { return next(new ldapjs.InvalidCredentialsError()); } 15 | res.end(); 16 | return next(); 17 | }); 18 | 19 | server.search('o=example', function (req, res, next) { 20 | if (provokeSearchError) { 21 | res.end(ldapjs.LDAP_SIZE_LIMIT_EXCEEDED); 22 | return next(); 23 | } 24 | const obj = { 25 | dn: req.dn.toString(), 26 | attributes: { 27 | objectclass: ['organization', 'top'], 28 | o: 'example', 29 | }, 30 | }; 31 | 32 | const group = { 33 | dn: req.dn.toString(), 34 | attributes: { 35 | objectClass: ['groupOfUniqueNames', 'top'], 36 | uniqueMember: ['uid=testuser, o=example'], 37 | cn: 'powerusers', 38 | ou: 'powerusers', 39 | }, 40 | }; 41 | 42 | if (req.filter.matches(obj.attributes)) { 43 | res.send(obj); 44 | } 45 | 46 | if (req.filter.matches(group.attributes)) { 47 | res.send(group); 48 | } 49 | res.end(); 50 | }); 51 | return new Promise(resolve => server.listen(port, () => resolve(server))); 52 | } 53 | 54 | module.exports = newServer; 55 | -------------------------------------------------------------------------------- /spec/support/MockPushAdapter.js: -------------------------------------------------------------------------------- 1 | module.exports = function (options) { 2 | return { 3 | options: options, 4 | send: function () {}, 5 | getValidPushTypes: function () { 6 | return Object.keys(options.options); 7 | }, 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /spec/support/cert/anothercert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE8DCCAtgCCQDjXCYv/hK1rjANBgkqhkiG9w0BAQsFADA5MRIwEAYDVQQDDAls 3 | b2NhbGhvc3QxIzAhBgkqhkiG9w0BCQEWFG5vLXJlcGx5QGV4YW1wbGUuY29tMCAX 4 | DTIwMTExNzEzMTAwMFoYDzIxMjAxMDI0MTMxMDAwWjA5MRIwEAYDVQQDDAlsb2Nh 5 | bGhvc3QxIzAhBgkqhkiG9w0BCQEWFG5vLXJlcGx5QGV4YW1wbGUuY29tMIICIjAN 6 | BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAmsOhxCNw3cEA3TLyqZXMI5p/LNSu 7 | W9doIvLEs1Ah8L/Gbl7xmSagkTZYzkTJDxITy0d45NVfmDsm0ctQrPV5MEbFE571 8 | lLQRnCFMpB3dqejfqQWpVCMfJKR1p8p5FTtcC5u5g7bcf2YeujwbUVDEtbeHwUeo 9 | XBnKfmv0UdGiLQf0uel5dcGWNp8dFo+hO4wCTA/risIdWawG8RHtzfhRIT2PqUa8 10 | ljgPyuPU2NQ19gUkV1LkXKJby+6VHhD6pSfzptbsJjalaGawTku7ZgBoZiax8wRk 11 | Bdwyd3ScMQg2VLGIn7YaMwb4ANtHqREekl0q7tPTu+PBmYqGXqa3lKa/s1OebUyS 12 | GQQXZB5T/Brm2fvJWqO9oJjZiTZzZIkBWDP0Cn+pmW/T4dADUms/vONEJE9IPFn1 13 | id5Q8vjSf5V1MaZJjWek38Y98xfYlKecHIqBAYQAydxdxuzG/DJu+2GzOZeffETD 14 | lzNwrLZp5lBzSrOwVntonvFo04lIq+DepVF+OqK8qV+7pnKCij5bGvdwxaY290pW 15 | +VTzK8kw0VUmpyYrDWIr7C52txaleY/AqsHy6wlVgdMbwXDjQ00twkJJT3tecL9I 16 | eWtLOuh7BeokvDFOXRVI2ZB2KN0sOBXsPfM6G4o9RK305Q9TFEXARnly9cwoV/i9 17 | 8yeJ5teQHw3dm7kCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAIWUqZSMCGlzWsCtU 18 | Xayr4qshfhqhm6PzgCWGjg8Xsm8gwrbYtQRwsKdJvw7ynLhbeX65i7p3/3AO0W6k 19 | 8Zpf58MHgepMOHmVT/KVBy7tUb83wJuoEvZzH50sO0rcA32c3p2FFUvt3JW+Dfo5 20 | BMX6GDlymtZPAplD9Rw5S5CXkZAgraDCbx1JMGFh0FfbP9v7jdo+so35y8UqmJ10 21 | 3U0NX2UJoWGE6RvV2P/1TE0v4pWyFzz1dF2k/gcmzYtMgIkJGGO8qhIGo2rSVJhC 22 | gVlYxyW/Rxogxz4wN0EqPIJNnkRby/g40OkPN8ATkHs09F4Jyax+cU0iJ3Hbn5t/ 23 | 0Ou5oaAs4t1+u11iahUMP6evaXooZONawM7h0RT4HHHZkXT95+kmaMz/+JZRp9DA 24 | Cafp9IsTjLzHvRy5DLX2kithqXaKRdpgTylx0qwW+8HxRjCcJEsFN3lXWqX12R8D 25 | OM8DnVsFX61Ygp7kTj2CQ+Y3Wqrj+jEkyJLRvMeTNPlxfazwudgFuDYsDErMCUwG 26 | U67vPoCkvIShFrnR9X4ojpG8aqWF8M/o8nvKIQp+FEW0Btm6rZT9lGba6nZw76Yj 27 | +48bsJCQ7UzhKkeFO4Bmj0fDkBTAElV2oEJXbHbB6+0DQE48uLWAr4xb7Vswph8c 28 | wHgxPsgsd2h0gr21doWB1BsdAu8= 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /spec/support/cert/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE8DCCAtgCCQDaLjopNQCJuTANBgkqhkiG9w0BAQsFADA5MRIwEAYDVQQDDAls 3 | b2NhbGhvc3QxIzAhBgkqhkiG9w0BCQEWFG5vLXJlcGx5QGV4YW1wbGUuY29tMCAX 4 | DTIwMTExNzExNDEzM1oYDzIxMjAxMDI0MTE0MTMzWjA5MRIwEAYDVQQDDAlsb2Nh 5 | bGhvc3QxIzAhBgkqhkiG9w0BCQEWFG5vLXJlcGx5QGV4YW1wbGUuY29tMIICIjAN 6 | BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvFf3I2RnIbp82Dd0AooAMamxMCgu 7 | g4zurMdA40mV8G+MA4Y5XFcGmOYT7LC94Z2nZ4tI+MNSiLKQY3Zq+OYGGmn/zVkr 8 | e8+02afxTjGmLVJWJXxXV2rsf8+UuJMOPbmVq87nJmD2gs9T6czOE3eQdDTRUzTg 9 | ubWhp3hV291gMfCIQeBbSqfbBscz0Nboj8NHStWDif5Io94l08tdW9oHIu99NYE0 10 | DMWIfBeztHpmSfkgPKH8lNar1dMsuCRW2Q/b01TNPKCNp8ZxyIhzkOq2gC5l60i5 11 | /iALWeEJii8g71V3DMbU5KoPEB+jFZ/z7qAi8TH9VqgaUycs/M96VXMIZbDhXywJ 12 | pg7qHxG/RT16bXwFotreThcla2M3VxsZEnYPEVmQEyVQeG7XyvqFMC3DhGCflW35 13 | dumJlkuGn9e9Lg6oiidp2RMnZuTsie+y3e3XJz2ZjFihGQNy2VzUrDz4ymi2fosV 14 | GMeHn3iK2nEqxf1mx021j3v40/8I5gtkS+zZuchclae0gRHaNN1tO0osedUdlV7D 15 | 0dvi9xezsfelqSqJjChLfl4R3HqC8k7cwUfK4RmKXhI5GX4ESr+1KWPIaqH5AxYB 16 | +ee2WYBQGhi6aXKpVcj9dvq+OAmDMPCJr0xnWMMZqR5dnxY1eEq2x28n2b1SyIw1 17 | +IctNX0nLwGAMgUCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAEYTLXvHWmNuYlG6e 18 | CAiK1ubJ98nO6PSJsl+qosB1kWKlPeWPOLLAeZxSDh0tKRPvQoXoH/AtMRGHFGLS 19 | lk7fbCAbgEqvfA9+8VhgpWSRXD2iodt444P+m93NiMNeusiRFzozXKZZvU4Ie97H 20 | mDuwLjpGgi8DUShebM2Ngif8t4DmSgSfLQ3OEac7oKUP6ffHMXbqnDwjh8ZCCh1m 21 | DN+0i4Y5WpKD7Z+JjGHJRm1Cx/G5pwP16Et6YejQMnNU70VDOzGSvNABmiexiR5p 22 | m8pOTkyxrYViYqamLZG5to5vpI6RmEoA/5vbU59dZ5DzPmSoyNbIeaz+dkSGoy6D 23 | SWKZMwGTf++xS5y+oy2lNS2iddc845qCcDy4jeel3N9JPlJPwrArfapATcrX3Rpy 24 | GsVPvWsKA3q7kwIQo3qscg0CkYwHo5VCnWHDNqgOeFo35J7y+CKxYRolD9/lCtAU 25 | Pw8CBGp1x8jgIv7yKNiPVDtWYztqfsFrplLf/yiZSH53zghSY3v5qnFRkmGq1HRC 26 | G6lz0yjI7RUEA2a/XA2dv9Hv6CdmWUzrsXvocH5VgQz2RtkyvSaLFzRv8gnESrY1 27 | 7qq55D1QIkO8UzzmCSpYPi5tUTGAYE1aHP/B1S5LpBrpaJ8Q9nfqA/9Bb+aho2ze 28 | N0vpdSSemKGQcrzquNqDJhUoXgQ= 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /spec/support/cert/game_center.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEvDCCA6SgAwIBAgIQXRHxNXkw1L9z5/3EZ/T/hDANBgkqhkiG9w0BAQsFADB/ 3 | MQswCQYDVQQGEwJVUzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xHzAd 4 | BgNVBAsTFlN5bWFudGVjIFRydXN0IE5ldHdvcmsxMDAuBgNVBAMTJ1N5bWFudGVj 5 | IENsYXNzIDMgU0hBMjU2IENvZGUgU2lnbmluZyBDQTAeFw0xODA5MTcwMDAwMDBa 6 | Fw0xOTA5MTcyMzU5NTlaMHMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9y 7 | bmlhMRIwEAYDVQQHDAlDdXBlcnRpbm8xFDASBgNVBAoMC0FwcGxlLCBJbmMuMQ8w 8 | DQYDVQQLDAZHQyBTUkUxFDASBgNVBAMMC0FwcGxlLCBJbmMuMIIBIjANBgkqhkiG 9 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEA06fwIi8fgKrTQu7cBcFkJVF6+Tqvkg7MKJTM 10 | IOYPPQtPF3AZYPsbUoRKAD7/JXrxxOSVJ7vU1mP77tYG8TcUteZ3sAwvt2dkRbm7 11 | ZO6DcmSggv1Dg4k3goNw4GYyCY4Z2/8JSmsQ80Iv/UOOwynpBziEeZmJ4uck6zlA 12 | 17cDkH48LBpKylaqthym5bFs9gj11pto7mvyb5BTcVuohwi6qosvbs/4VGbC2Nsz 13 | ie416nUZfv+xxoXH995gxR2mw5cDdeCew7pSKxEhvYjT2nVdQF0q/hnPMFnOaEyT 14 | q79n3gwFXyt0dy8eP6KBF7EW9J6b7ubu/j7h+tQfxPM+gTXOBQIDAQABo4IBPjCC 15 | ATowCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUH 16 | AwMwYQYDVR0gBFowWDBWBgZngQwBBAEwTDAjBggrBgEFBQcCARYXaHR0cHM6Ly9k 17 | LnN5bWNiLmNvbS9jcHMwJQYIKwYBBQUHAgIwGQwXaHR0cHM6Ly9kLnN5bWNiLmNv 18 | bS9ycGEwHwYDVR0jBBgwFoAUljtT8Hkzl699g+8uK8zKt4YecmYwKwYDVR0fBCQw 19 | IjAgoB6gHIYaaHR0cDovL3N2LnN5bWNiLmNvbS9zdi5jcmwwVwYIKwYBBQUHAQEE 20 | SzBJMB8GCCsGAQUFBzABhhNodHRwOi8vc3Yuc3ltY2QuY29tMCYGCCsGAQUFBzAC 21 | hhpodHRwOi8vc3Yuc3ltY2IuY29tL3N2LmNydDANBgkqhkiG9w0BAQsFAAOCAQEA 22 | I/j/PcCNPebSAGrcqSFBSa2mmbusOX01eVBg8X0G/z8Z+ZWUfGFzDG0GQf89MPxV 23 | woec+nZuqui7o9Bg8s8JbHV0TC52X14CbTj9w/qBF748WbH9gAaTkrJYPm+MlNhu 24 | tjEuQdNl/YXVMvQW4O8UMHTi09GyJQ0NC4q92Wxvx1m/qzjvTLvrXHGQ9pEHhPyz 25 | vfBLxQkWpNoCNKU7UeESyH06XOrGc9MsII9deeKsDJp9a0jtx+pP4MFVtFME9SSQ 26 | tMBs0It7WwEf7qcRLpialxKwY2EzQ9g4WnANHqo18PrDBE10TFpZPzUh7JhMViVr 27 | EEbl0YdElmF8Hlamah/yNw== 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /spec/support/cert/gc-prod-4.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parse-community/parse-server/4626d35aa6fadd6a66386ce19e7a96e5ad165aeb/spec/support/cert/gc-prod-4.cer -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": ["**/*.[sS]pec.js"], 4 | "helpers": ["helper.js"], 5 | "random": true 6 | } 7 | -------------------------------------------------------------------------------- /spec/support/lorem.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus lobortis semper diam, ac euismod diam pharetra sed. Etiam eget efficitur neque. Proin nec diam mi. Sed ut purus dolor. Nulla nulla nibh, ornare vitae ornare et, scelerisque rutrum eros. Mauris venenatis tincidunt turpis a mollis. Donec gravida eget enim in luctus. 2 | 3 | Sed porttitor commodo orci, ut pretium eros convallis eget. Curabitur pretium velit in odio dictum luctus. Vivamus ac tristique arcu, a semper tellus. Morbi euismod purus dapibus vestibulum sagittis. Nunc dapibus vehicula leo at scelerisque. Donec porta mauris quis nulla imperdiet consectetur. Curabitur sagittis eleifend arcu eget elementum. Aenean interdum tincidunt ornare. Pellentesque sit amet interdum tortor. Pellentesque blandit nisl eget euismod consequat. Etiam feugiat felis sit amet porta pulvinar. Lorem ipsum dolor sit amet, consectetur adipiscing elit. 4 | 5 | Nulla faucibus sem ipsum, at rhoncus diam pulvinar at. Vivamus consectetur, diam at aliquet vestibulum, sem purus elementum nulla, eget tincidunt nullam. 6 | -------------------------------------------------------------------------------- /spec/support/myoauth.js: -------------------------------------------------------------------------------- 1 | // Custom oauth provider by module 2 | 3 | // Returns a promise that fulfills iff this user id is valid. 4 | function validateAuthData(authData) { 5 | if (authData.id == '12345' && authData.access_token == '12345') { 6 | return Promise.resolve(); 7 | } 8 | return Promise.reject(); 9 | } 10 | function validateAppId() { 11 | return Promise.resolve(); 12 | } 13 | 14 | module.exports = { 15 | validateAppId: validateAppId, 16 | validateAuthData: validateAuthData, 17 | }; 18 | -------------------------------------------------------------------------------- /src/Adapters/AdapterLoader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module AdapterLoader 3 | */ 4 | /** 5 | * @static 6 | * Attempt to load an adapter or fallback to the default. 7 | * @param {Adapter} adapter an adapter 8 | * @param {Adapter} defaultAdapter the default adapter to load 9 | * @param {any} options options to pass to the contstructor 10 | * @returns {Object} the loaded adapter 11 | */ 12 | export function loadAdapter(adapter, defaultAdapter, options): T { 13 | if (!adapter) { 14 | if (!defaultAdapter) { 15 | return options; 16 | } 17 | // Load from the default adapter when no adapter is set 18 | return loadAdapter(defaultAdapter, undefined, options); 19 | } else if (typeof adapter === 'function') { 20 | try { 21 | return adapter(options); 22 | } catch (e) { 23 | if (e.name === 'TypeError') { 24 | var Adapter = adapter; 25 | return new Adapter(options); 26 | } else { 27 | throw e; 28 | } 29 | } 30 | } else if (typeof adapter === 'string') { 31 | adapter = require(adapter); 32 | // If it's define as a module, get the default 33 | if (adapter.default) { 34 | adapter = adapter.default; 35 | } 36 | return loadAdapter(adapter, undefined, options); 37 | } else if (adapter.module) { 38 | return loadAdapter(adapter.module, undefined, adapter.options); 39 | } else if (adapter.class) { 40 | return loadAdapter(adapter.class, undefined, adapter.options); 41 | } else if (adapter.adapter) { 42 | return loadAdapter(adapter.adapter, undefined, adapter.options); 43 | } 44 | // return the adapter as provided 45 | return adapter; 46 | } 47 | 48 | export async function loadModule(modulePath) { 49 | const module = await import(modulePath); 50 | return module?.default || module; 51 | } 52 | 53 | export default loadAdapter; 54 | -------------------------------------------------------------------------------- /src/Adapters/Analytics/AnalyticsAdapter.js: -------------------------------------------------------------------------------- 1 | /*eslint no-unused-vars: "off"*/ 2 | /** 3 | * @interface AnalyticsAdapter 4 | * @module Adapters 5 | */ 6 | export class AnalyticsAdapter { 7 | /** 8 | @param {any} parameters: the analytics request body, analytics info will be in the dimensions property 9 | @param {Request} req: the original http request 10 | */ 11 | appOpened(parameters, req) { 12 | return Promise.resolve({}); 13 | } 14 | 15 | /** 16 | @param {String} eventName: the name of the custom eventName 17 | @param {any} parameters: the analytics request body, analytics info will be in the dimensions property 18 | @param {Request} req: the original http request 19 | */ 20 | trackEvent(eventName, parameters, req) { 21 | return Promise.resolve({}); 22 | } 23 | } 24 | 25 | export default AnalyticsAdapter; 26 | -------------------------------------------------------------------------------- /src/Adapters/Auth/httpsRequest.js: -------------------------------------------------------------------------------- 1 | const https = require('https'); 2 | 3 | function makeCallback(resolve, reject, noJSON) { 4 | return function (res) { 5 | let data = ''; 6 | res.on('data', chunk => { 7 | data += chunk; 8 | }); 9 | res.on('end', () => { 10 | if (noJSON) { 11 | return resolve(data); 12 | } 13 | try { 14 | data = JSON.parse(data); 15 | } catch (e) { 16 | return reject(e); 17 | } 18 | resolve(data); 19 | }); 20 | res.on('error', reject); 21 | }; 22 | } 23 | 24 | function get(options, noJSON = false) { 25 | return new Promise((resolve, reject) => { 26 | https.get(options, makeCallback(resolve, reject, noJSON)).on('error', reject); 27 | }); 28 | } 29 | 30 | function request(options, postData) { 31 | return new Promise((resolve, reject) => { 32 | const req = https.request(options, makeCallback(resolve, reject)); 33 | req.on('error', reject); 34 | req.write(postData); 35 | req.end(); 36 | }); 37 | } 38 | 39 | module.exports = { get, request }; 40 | -------------------------------------------------------------------------------- /src/Adapters/Auth/janrainengage.js: -------------------------------------------------------------------------------- 1 | // Helper functions for accessing the Janrain Engage API. 2 | var httpsRequest = require('./httpsRequest'); 3 | var Parse = require('parse/node').Parse; 4 | var querystring = require('querystring'); 5 | import Config from '../../Config'; 6 | import Deprecator from '../../Deprecator/Deprecator'; 7 | 8 | // Returns a promise that fulfills iff this user id is valid. 9 | function validateAuthData(authData, options) { 10 | const config = Config.get(Parse.applicationId); 11 | 12 | Deprecator.logRuntimeDeprecation({ usage: 'janrainengage adapter' }); 13 | if (!config?.auth?.janrainengage?.enableInsecureAuth || !config.enableInsecureAuthAdapters) { 14 | throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'janrainengage adapter only works with enableInsecureAuth: true'); 15 | } 16 | 17 | return apiRequest(options.api_key, authData.auth_token).then(data => { 18 | //successful response will have a "stat" (status) of 'ok' and a profile node with an identifier 19 | //see: http://developers.janrain.com/overview/social-login/identity-providers/user-profile-data/#normalized-user-profile-data 20 | if (data && data.stat == 'ok' && data.profile.identifier == authData.id) { 21 | return; 22 | } 23 | throw new Parse.Error( 24 | Parse.Error.OBJECT_NOT_FOUND, 25 | 'Janrain engage auth is invalid for this user.' 26 | ); 27 | }); 28 | } 29 | 30 | // Returns a promise that fulfills iff this app id is valid. 31 | function validateAppId() { 32 | //no-op 33 | return Promise.resolve(); 34 | } 35 | 36 | // A promisey wrapper for api requests 37 | function apiRequest(api_key, auth_token) { 38 | var post_data = querystring.stringify({ 39 | token: auth_token, 40 | apiKey: api_key, 41 | format: 'json', 42 | }); 43 | 44 | var post_options = { 45 | host: 'rpxnow.com', 46 | path: '/api/v2/auth_info', 47 | method: 'POST', 48 | headers: { 49 | 'Content-Type': 'application/x-www-form-urlencoded', 50 | 'Content-Length': post_data.length, 51 | }, 52 | }; 53 | 54 | return httpsRequest.request(post_options, post_data); 55 | } 56 | 57 | module.exports = { 58 | validateAppId: validateAppId, 59 | validateAuthData: validateAuthData, 60 | }; 61 | -------------------------------------------------------------------------------- /src/Adapters/Auth/meetup.js: -------------------------------------------------------------------------------- 1 | // Helper functions for accessing the meetup API. 2 | var Parse = require('parse/node').Parse; 3 | const httpsRequest = require('./httpsRequest'); 4 | import Config from '../../Config'; 5 | import Deprecator from '../../Deprecator/Deprecator'; 6 | 7 | // Returns a promise that fulfills iff this user id is valid. 8 | async function validateAuthData(authData) { 9 | const config = Config.get(Parse.applicationId); 10 | const meetupConfig = config.auth.meetup; 11 | 12 | Deprecator.logRuntimeDeprecation({ usage: 'meetup adapter' }); 13 | 14 | if (!meetupConfig?.enableInsecureAuth) { 15 | throw new Parse.Error('Meetup only works with enableInsecureAuth: true'); 16 | } 17 | 18 | const data = await request('member/self', authData.access_token); 19 | if (data?.id !== authData.id) { 20 | throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Meetup auth is invalid for this user.'); 21 | } 22 | } 23 | 24 | // Returns a promise that fulfills iff this app id is valid. 25 | function validateAppId() { 26 | return Promise.resolve(); 27 | } 28 | 29 | // A promisey wrapper for api requests 30 | function request(path, access_token) { 31 | return httpsRequest.get({ 32 | host: 'api.meetup.com', 33 | path: '/2/' + path, 34 | headers: { 35 | Authorization: 'bearer ' + access_token, 36 | }, 37 | }); 38 | } 39 | 40 | module.exports = { 41 | validateAppId: validateAppId, 42 | validateAuthData: validateAuthData, 43 | }; 44 | -------------------------------------------------------------------------------- /src/Adapters/Auth/phantauth.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PhantAuth was designed to simplify testing for applications using OpenID Connect 3 | * authentication by making use of random generated users. 4 | * 5 | * To learn more, please go to: https://www.phantauth.net 6 | */ 7 | 8 | const { Parse } = require('parse/node'); 9 | const httpsRequest = require('./httpsRequest'); 10 | import Config from '../../Config'; 11 | import Deprecator from '../../Deprecator/Deprecator'; 12 | 13 | // Returns a promise that fulfills if this user id is valid. 14 | async function validateAuthData(authData) { 15 | const config = Config.get(Parse.applicationId); 16 | 17 | Deprecator.logRuntimeDeprecation({ usage: 'phantauth adapter' }); 18 | 19 | const phantauthConfig = config.auth.phantauth; 20 | if (!phantauthConfig?.enableInsecureAuth) { 21 | throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'PhantAuth only works with enableInsecureAuth: true'); 22 | } 23 | 24 | const data = await request('auth/userinfo', authData.access_token); 25 | if (data?.sub !== authData.id) { 26 | throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'PhantAuth auth is invalid for this user.'); 27 | } 28 | } 29 | 30 | // Returns a promise that fulfills if this app id is valid. 31 | function validateAppId() { 32 | return Promise.resolve(); 33 | } 34 | 35 | // A promisey wrapper for api requests 36 | function request(path, access_token) { 37 | return httpsRequest.get({ 38 | host: 'phantauth.net', 39 | path: '/' + path, 40 | headers: { 41 | Authorization: 'bearer ' + access_token, 42 | 'User-Agent': 'parse-server', 43 | }, 44 | }); 45 | } 46 | 47 | module.exports = { 48 | validateAppId: validateAppId, 49 | validateAuthData: validateAuthData, 50 | }; 51 | -------------------------------------------------------------------------------- /src/Adapters/Auth/utils.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const util = require('util'); 3 | const Parse = require('parse/node').Parse; 4 | const getHeaderFromToken = token => { 5 | const decodedToken = jwt.decode(token, { complete: true }); 6 | if (!decodedToken) { 7 | throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `provided token does not decode as JWT`); 8 | } 9 | 10 | return decodedToken.header; 11 | }; 12 | 13 | /** 14 | * Returns the signing key from a JWKS client. 15 | * @param {Object} client The JWKS client. 16 | * @param {String} key The kid. 17 | */ 18 | async function getSigningKey(client, key) { 19 | return util.promisify(client.getSigningKey)(key); 20 | } 21 | module.exports = { 22 | getHeaderFromToken, 23 | getSigningKey, 24 | }; 25 | -------------------------------------------------------------------------------- /src/Adapters/Cache/CacheAdapter.js: -------------------------------------------------------------------------------- 1 | /*eslint no-unused-vars: "off"*/ 2 | /** 3 | * @interface 4 | * @memberof module:Adapters 5 | */ 6 | export class CacheAdapter { 7 | /** 8 | * Get a value in the cache 9 | * @param {String} key Cache key to get 10 | * @return {Promise} that will eventually resolve to the value in the cache. 11 | */ 12 | get(key) {} 13 | 14 | /** 15 | * Set a value in the cache 16 | * @param {String} key Cache key to set 17 | * @param {String} value Value to set the key 18 | * @param {String} ttl Optional TTL 19 | */ 20 | put(key, value, ttl) {} 21 | 22 | /** 23 | * Remove a value from the cache. 24 | * @param {String} key Cache key to remove 25 | */ 26 | del(key) {} 27 | 28 | /** 29 | * Empty a cache 30 | */ 31 | clear() {} 32 | } 33 | -------------------------------------------------------------------------------- /src/Adapters/Cache/InMemoryCache.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_CACHE_TTL = 5 * 1000; 2 | 3 | export class InMemoryCache { 4 | constructor({ ttl = DEFAULT_CACHE_TTL }) { 5 | this.ttl = ttl; 6 | this.cache = Object.create(null); 7 | } 8 | 9 | get(key) { 10 | const record = this.cache[key]; 11 | if (record == null) { 12 | return null; 13 | } 14 | 15 | // Has Record and isnt expired 16 | if (isNaN(record.expire) || record.expire >= Date.now()) { 17 | return record.value; 18 | } 19 | 20 | // Record has expired 21 | delete this.cache[key]; 22 | return null; 23 | } 24 | 25 | put(key, value, ttl = this.ttl) { 26 | if (ttl < 0 || isNaN(ttl)) { 27 | ttl = NaN; 28 | } 29 | 30 | var record = { 31 | value: value, 32 | expire: ttl + Date.now(), 33 | }; 34 | 35 | if (!isNaN(record.expire)) { 36 | record.timeout = setTimeout(() => { 37 | this.del(key); 38 | }, ttl); 39 | } 40 | 41 | this.cache[key] = record; 42 | } 43 | 44 | del(key) { 45 | var record = this.cache[key]; 46 | if (record == null) { 47 | return; 48 | } 49 | 50 | if (record.timeout) { 51 | clearTimeout(record.timeout); 52 | } 53 | delete this.cache[key]; 54 | } 55 | 56 | clear() { 57 | this.cache = Object.create(null); 58 | } 59 | } 60 | 61 | export default InMemoryCache; 62 | -------------------------------------------------------------------------------- /src/Adapters/Cache/InMemoryCacheAdapter.js: -------------------------------------------------------------------------------- 1 | import { LRUCache } from './LRUCache'; 2 | 3 | export class InMemoryCacheAdapter { 4 | constructor(ctx) { 5 | this.cache = new LRUCache(ctx); 6 | } 7 | 8 | get(key) { 9 | const record = this.cache.get(key); 10 | if (record === null) { 11 | return Promise.resolve(null); 12 | } 13 | return Promise.resolve(record); 14 | } 15 | 16 | put(key, value, ttl) { 17 | this.cache.put(key, value, ttl); 18 | return Promise.resolve(); 19 | } 20 | 21 | del(key) { 22 | this.cache.del(key); 23 | return Promise.resolve(); 24 | } 25 | 26 | clear() { 27 | this.cache.clear(); 28 | return Promise.resolve(); 29 | } 30 | } 31 | 32 | export default InMemoryCacheAdapter; 33 | -------------------------------------------------------------------------------- /src/Adapters/Cache/LRUCache.js: -------------------------------------------------------------------------------- 1 | import { LRUCache as LRU } from 'lru-cache'; 2 | import defaults from '../../defaults'; 3 | 4 | export class LRUCache { 5 | constructor({ ttl = defaults.cacheTTL, maxSize = defaults.cacheMaxSize }) { 6 | this.cache = new LRU({ 7 | max: maxSize, 8 | ttl, 9 | }); 10 | } 11 | 12 | get(key) { 13 | return this.cache.get(key) || null; 14 | } 15 | 16 | put(key, value, ttl = this.ttl) { 17 | this.cache.set(key, value, ttl); 18 | } 19 | 20 | del(key) { 21 | this.cache.delete(key); 22 | } 23 | 24 | clear() { 25 | this.cache.clear(); 26 | } 27 | } 28 | 29 | export default LRUCache; 30 | -------------------------------------------------------------------------------- /src/Adapters/Cache/NullCacheAdapter.js: -------------------------------------------------------------------------------- 1 | export class NullCacheAdapter { 2 | constructor() {} 3 | 4 | get() { 5 | return new Promise(resolve => { 6 | return resolve(null); 7 | }); 8 | } 9 | 10 | put() { 11 | return Promise.resolve(); 12 | } 13 | 14 | del() { 15 | return Promise.resolve(); 16 | } 17 | 18 | clear() { 19 | return Promise.resolve(); 20 | } 21 | } 22 | 23 | export default NullCacheAdapter; 24 | -------------------------------------------------------------------------------- /src/Adapters/Cache/SchemaCache.js: -------------------------------------------------------------------------------- 1 | const SchemaCache = {}; 2 | 3 | export default { 4 | all() { 5 | return [...(SchemaCache.allClasses || [])]; 6 | }, 7 | 8 | get(className) { 9 | return this.all().find(cached => cached.className === className); 10 | }, 11 | 12 | put(allSchema) { 13 | SchemaCache.allClasses = allSchema; 14 | }, 15 | 16 | del(className) { 17 | this.put(this.all().filter(cached => cached.className !== className)); 18 | }, 19 | 20 | clear() { 21 | delete SchemaCache.allClasses; 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/Adapters/Email/MailAdapter.js: -------------------------------------------------------------------------------- 1 | /*eslint no-unused-vars: "off"*/ 2 | /** 3 | * @interface 4 | * @memberof module:Adapters 5 | * Mail Adapter prototype 6 | * A MailAdapter should implement at least sendMail() 7 | */ 8 | export class MailAdapter { 9 | /** 10 | * A method for sending mail 11 | * @param options would have the parameters 12 | * - to: the recipient 13 | * - text: the raw text of the message 14 | * - subject: the subject of the email 15 | */ 16 | sendMail(options) {} 17 | 18 | /* You can implement those methods if you want 19 | * to provide HTML templates etc... 20 | */ 21 | // sendVerificationEmail({ link, appName, user }) {} 22 | // sendPasswordResetEmail({ link, appName, user }) {} 23 | } 24 | 25 | export default MailAdapter; 26 | -------------------------------------------------------------------------------- /src/Adapters/Files/GridStoreAdapter.js: -------------------------------------------------------------------------------- 1 | // Note: GridStore was replaced by GridFSBucketAdapter by default in 2018 by @flovilmart 2 | throw new Error( 3 | 'GridStoreAdapter: GridStore is no longer supported by parse server and mongodb, use GridFSBucketAdapter instead.' 4 | ); 5 | -------------------------------------------------------------------------------- /src/Adapters/Logger/LoggerAdapter.js: -------------------------------------------------------------------------------- 1 | /*eslint no-unused-vars: "off"*/ 2 | /** 3 | * @interface 4 | * @memberof module:Adapters 5 | * Logger Adapter 6 | * Allows you to change the logger mechanism 7 | * Default is WinstonLoggerAdapter.js 8 | */ 9 | export class LoggerAdapter { 10 | constructor(options) {} 11 | /** 12 | * log 13 | * @param {String} level 14 | * @param {String} message 15 | * @param {Object} metadata 16 | */ 17 | log(level, message /* meta */) {} 18 | } 19 | 20 | export default LoggerAdapter; 21 | -------------------------------------------------------------------------------- /src/Adapters/Logger/WinstonLoggerAdapter.js: -------------------------------------------------------------------------------- 1 | import { LoggerAdapter } from './LoggerAdapter'; 2 | import { logger, addTransport, configureLogger } from './WinstonLogger'; 3 | 4 | const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000; 5 | 6 | export class WinstonLoggerAdapter extends LoggerAdapter { 7 | constructor(options) { 8 | super(); 9 | if (options) { 10 | configureLogger(options); 11 | } 12 | } 13 | 14 | log() { 15 | return logger.log.apply(logger, arguments); 16 | } 17 | 18 | addTransport(transport) { 19 | // Note that this is calling addTransport 20 | // from logger. See import - confusing. 21 | // but this is not recursive. 22 | addTransport(transport); 23 | } 24 | 25 | // custom query as winston is currently limited 26 | query(options, callback = () => {}) { 27 | if (!options) { 28 | options = {}; 29 | } 30 | // defaults to 7 days prior 31 | const from = options.from || new Date(Date.now() - 7 * MILLISECONDS_IN_A_DAY); 32 | const until = options.until || new Date(); 33 | const limit = options.size || 10; 34 | const order = options.order || 'desc'; 35 | const level = options.level || 'info'; 36 | 37 | const queryOptions = { 38 | from, 39 | until, 40 | limit, 41 | order, 42 | }; 43 | 44 | return new Promise((resolve, reject) => { 45 | logger.query(queryOptions, (err, res) => { 46 | if (err) { 47 | callback(err); 48 | return reject(err); 49 | } 50 | 51 | if (level === 'error') { 52 | callback(res['parse-server-error']); 53 | resolve(res['parse-server-error']); 54 | } else { 55 | callback(res['parse-server']); 56 | resolve(res['parse-server']); 57 | } 58 | }); 59 | }); 60 | } 61 | } 62 | 63 | export default WinstonLoggerAdapter; 64 | -------------------------------------------------------------------------------- /src/Adapters/MessageQueue/EventEmitterMQ.js: -------------------------------------------------------------------------------- 1 | import events from 'events'; 2 | 3 | const emitter = new events.EventEmitter(); 4 | const subscriptions = new Map(); 5 | 6 | function unsubscribe(channel: string) { 7 | if (!subscriptions.has(channel)) { 8 | //console.log('No channel to unsub from'); 9 | return; 10 | } 11 | //console.log('unsub ', channel); 12 | emitter.removeListener(channel, subscriptions.get(channel)); 13 | subscriptions.delete(channel); 14 | } 15 | 16 | class Publisher { 17 | emitter: any; 18 | 19 | constructor(emitter: any) { 20 | this.emitter = emitter; 21 | } 22 | 23 | publish(channel: string, message: string): void { 24 | this.emitter.emit(channel, message); 25 | } 26 | } 27 | 28 | class Consumer extends events.EventEmitter { 29 | emitter: any; 30 | 31 | constructor(emitter: any) { 32 | super(); 33 | this.emitter = emitter; 34 | } 35 | 36 | subscribe(channel: string): void { 37 | unsubscribe(channel); 38 | const handler = message => { 39 | this.emit('message', channel, message); 40 | }; 41 | subscriptions.set(channel, handler); 42 | this.emitter.on(channel, handler); 43 | } 44 | 45 | unsubscribe(channel: string): void { 46 | unsubscribe(channel); 47 | } 48 | } 49 | 50 | function createPublisher(): any { 51 | return new Publisher(emitter); 52 | } 53 | 54 | function createSubscriber(): any { 55 | return new Consumer(emitter); 56 | } 57 | 58 | const EventEmitterMQ = { 59 | createPublisher, 60 | createSubscriber, 61 | }; 62 | 63 | export { EventEmitterMQ }; 64 | -------------------------------------------------------------------------------- /src/Adapters/PubSub/EventEmitterPubSub.js: -------------------------------------------------------------------------------- 1 | import events from 'events'; 2 | 3 | const emitter = new events.EventEmitter(); 4 | 5 | class Publisher { 6 | emitter: any; 7 | 8 | constructor(emitter: any) { 9 | this.emitter = emitter; 10 | } 11 | 12 | publish(channel: string, message: string): void { 13 | this.emitter.emit(channel, message); 14 | } 15 | } 16 | 17 | class Subscriber extends events.EventEmitter { 18 | emitter: any; 19 | subscriptions: any; 20 | 21 | constructor(emitter: any) { 22 | super(); 23 | this.emitter = emitter; 24 | this.subscriptions = new Map(); 25 | } 26 | 27 | subscribe(channel: string): void { 28 | const handler = message => { 29 | this.emit('message', channel, message); 30 | }; 31 | this.subscriptions.set(channel, handler); 32 | this.emitter.on(channel, handler); 33 | } 34 | 35 | unsubscribe(channel: string): void { 36 | if (!this.subscriptions.has(channel)) { 37 | return; 38 | } 39 | this.emitter.removeListener(channel, this.subscriptions.get(channel)); 40 | this.subscriptions.delete(channel); 41 | } 42 | } 43 | 44 | function createPublisher(): any { 45 | return new Publisher(emitter); 46 | } 47 | 48 | function createSubscriber(): any { 49 | // createSubscriber is called once at live query server start 50 | // to avoid max listeners warning, we should clean up the event emitter 51 | // each time this function is called 52 | emitter.removeAllListeners(); 53 | return new Subscriber(emitter); 54 | } 55 | 56 | const EventEmitterPubSub = { 57 | createPublisher, 58 | createSubscriber, 59 | }; 60 | 61 | export { EventEmitterPubSub }; 62 | -------------------------------------------------------------------------------- /src/Adapters/PubSub/PubSubAdapter.js: -------------------------------------------------------------------------------- 1 | /*eslint no-unused-vars: "off"*/ 2 | /** 3 | * @interface 4 | * @memberof module:Adapters 5 | */ 6 | export class PubSubAdapter { 7 | /** 8 | * @returns {PubSubAdapter.Publisher} 9 | */ 10 | static createPublisher() {} 11 | /** 12 | * @returns {PubSubAdapter.Subscriber} 13 | */ 14 | static createSubscriber() {} 15 | } 16 | 17 | /** 18 | * @interface Publisher 19 | * @memberof PubSubAdapter 20 | */ 21 | interface Publisher { 22 | /** 23 | * @param {String} channel the channel in which to publish 24 | * @param {String} message the message to publish 25 | */ 26 | publish(channel: string, message: string): void; 27 | } 28 | 29 | /** 30 | * @interface Subscriber 31 | * @memberof PubSubAdapter 32 | */ 33 | interface Subscriber { 34 | /** 35 | * called when a new subscription the channel is required 36 | * @param {String} channel the channel to subscribe 37 | */ 38 | subscribe(channel: string): void; 39 | 40 | /** 41 | * called when the subscription from the channel should be stopped 42 | * @param {String} channel 43 | */ 44 | unsubscribe(channel: string): void; 45 | } 46 | 47 | export default PubSubAdapter; 48 | -------------------------------------------------------------------------------- /src/Adapters/PubSub/RedisPubSub.js: -------------------------------------------------------------------------------- 1 | import { createClient } from 'redis'; 2 | import { logger } from '../../logger'; 3 | 4 | function createPublisher({ redisURL, redisOptions = {} }): any { 5 | redisOptions.no_ready_check = true; 6 | const client = createClient({ url: redisURL, ...redisOptions }); 7 | client.on('error', err => { logger.error('RedisPubSub Publisher client error', { error: err }) }); 8 | client.on('connect', () => {}); 9 | client.on('reconnecting', () => {}); 10 | client.on('ready', () => {}); 11 | return client; 12 | } 13 | 14 | function createSubscriber({ redisURL, redisOptions = {} }): any { 15 | redisOptions.no_ready_check = true; 16 | const client = createClient({ url: redisURL, ...redisOptions }); 17 | client.on('error', err => { logger.error('RedisPubSub Subscriber client error', { error: err }) }); 18 | client.on('connect', () => {}); 19 | client.on('reconnecting', () => {}); 20 | client.on('ready', () => {}); 21 | return client; 22 | } 23 | 24 | const RedisPubSub = { 25 | createPublisher, 26 | createSubscriber, 27 | }; 28 | 29 | export { RedisPubSub }; 30 | -------------------------------------------------------------------------------- /src/Adapters/Push/PushAdapter.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /*eslint no-unused-vars: "off"*/ 3 | // Push Adapter 4 | // 5 | // Allows you to change the push notification mechanism. 6 | // 7 | // Adapter classes must implement the following functions: 8 | // * getValidPushTypes() 9 | // * send(devices, installations, pushStatus) 10 | // 11 | // Default is ParsePushAdapter, which uses GCM for 12 | // android push and APNS for ios push. 13 | 14 | /** 15 | * @interface 16 | * @memberof module:Adapters 17 | */ 18 | export class PushAdapter { 19 | /** 20 | * @param {any} body 21 | * @param {Parse.Installation[]} installations 22 | * @param {any} pushStatus 23 | * @returns {Promise} 24 | */ 25 | send(body: any, installations: any[], pushStatus: any): ?Promise<*> {} 26 | 27 | /** 28 | * Get an array of valid push types. 29 | * @returns {Array} An array of valid push types 30 | */ 31 | getValidPushTypes(): string[] { 32 | return []; 33 | } 34 | } 35 | 36 | export default PushAdapter; 37 | -------------------------------------------------------------------------------- /src/Adapters/Storage/Postgres/PostgresClient.js: -------------------------------------------------------------------------------- 1 | const parser = require('./PostgresConfigParser'); 2 | 3 | export function createClient(uri, databaseOptions) { 4 | let dbOptions = {}; 5 | databaseOptions = databaseOptions || {}; 6 | 7 | if (uri) { 8 | dbOptions = parser.getDatabaseOptionsFromURI(uri); 9 | } 10 | 11 | for (const key in databaseOptions) { 12 | dbOptions[key] = databaseOptions[key]; 13 | } 14 | 15 | const initOptions = dbOptions.initOptions || {}; 16 | initOptions.noWarnings = process && process.env.TESTING; 17 | 18 | const pgp = require('pg-promise')(initOptions); 19 | const client = pgp(dbOptions); 20 | 21 | if (process.env.PARSE_SERVER_LOG_LEVEL === 'debug') { 22 | const monitor = require('pg-monitor'); 23 | if (monitor.isAttached()) { 24 | monitor.detach(); 25 | } 26 | monitor.attach(initOptions); 27 | } 28 | 29 | if (dbOptions.pgOptions) { 30 | for (const key in dbOptions.pgOptions) { 31 | pgp.pg.defaults[key] = dbOptions.pgOptions[key]; 32 | } 33 | } 34 | 35 | return { client, pgp }; 36 | } 37 | -------------------------------------------------------------------------------- /src/Adapters/Storage/Postgres/sql/array/add-unique.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION array_add_unique( 2 | "array" jsonb, 3 | "values" jsonb 4 | ) 5 | RETURNS jsonb 6 | LANGUAGE sql 7 | IMMUTABLE 8 | STRICT 9 | AS $function$ 10 | SELECT array_to_json(ARRAY(SELECT DISTINCT unnest(ARRAY(SELECT DISTINCT jsonb_array_elements("array")) || ARRAY(SELECT DISTINCT jsonb_array_elements("values")))))::jsonb; 11 | $function$; 12 | -------------------------------------------------------------------------------- /src/Adapters/Storage/Postgres/sql/array/add.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION array_add( 2 | "array" jsonb, 3 | "values" jsonb 4 | ) 5 | RETURNS jsonb 6 | LANGUAGE sql 7 | IMMUTABLE 8 | STRICT 9 | AS $function$ 10 | SELECT array_to_json(ARRAY(SELECT unnest(ARRAY(SELECT DISTINCT jsonb_array_elements("array")) || ARRAY(SELECT jsonb_array_elements("values")))))::jsonb; 11 | $function$; 12 | -------------------------------------------------------------------------------- /src/Adapters/Storage/Postgres/sql/array/contains-all-regex.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION array_contains_all_regex( 2 | "array" jsonb, 3 | "values" jsonb 4 | ) 5 | RETURNS boolean 6 | LANGUAGE sql 7 | IMMUTABLE 8 | STRICT 9 | AS $function$ 10 | SELECT CASE 11 | WHEN 0 = jsonb_array_length("values") THEN true = false 12 | ELSE (SELECT RES.CNT = jsonb_array_length("values") FROM (SELECT COUNT(*) as CNT FROM jsonb_array_elements_text("array") as elt WHERE elt LIKE ANY (SELECT jsonb_array_elements_text("values"))) as RES) 13 | END; 14 | $function$; -------------------------------------------------------------------------------- /src/Adapters/Storage/Postgres/sql/array/contains-all.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION array_contains_all( 2 | "array" jsonb, 3 | "values" jsonb 4 | ) 5 | RETURNS boolean 6 | LANGUAGE sql 7 | IMMUTABLE 8 | STRICT 9 | AS $function$ 10 | SELECT CASE 11 | WHEN 0 = jsonb_array_length("values") THEN true = false 12 | ELSE (SELECT RES.CNT = jsonb_array_length("values") FROM (SELECT COUNT(*) as CNT FROM jsonb_array_elements_text("array") as elt WHERE elt IN (SELECT jsonb_array_elements_text("values"))) as RES) 13 | END; 14 | $function$; 15 | -------------------------------------------------------------------------------- /src/Adapters/Storage/Postgres/sql/array/contains.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION array_contains( 2 | "array" jsonb, 3 | "values" jsonb 4 | ) 5 | RETURNS boolean 6 | LANGUAGE sql 7 | IMMUTABLE 8 | STRICT 9 | AS $function$ 10 | SELECT RES.CNT >= 1 FROM (SELECT COUNT(*) as CNT FROM jsonb_array_elements("array") as elt WHERE elt IN (SELECT jsonb_array_elements("values"))) as RES; 11 | $function$; 12 | -------------------------------------------------------------------------------- /src/Adapters/Storage/Postgres/sql/array/remove.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION array_remove( 2 | "array" jsonb, 3 | "values" jsonb 4 | ) 5 | RETURNS jsonb 6 | LANGUAGE sql 7 | IMMUTABLE 8 | STRICT 9 | AS $function$ 10 | SELECT array_to_json(ARRAY(SELECT * FROM jsonb_array_elements("array") as elt WHERE elt NOT IN (SELECT * FROM (SELECT jsonb_array_elements("values")) AS sub)))::jsonb; 11 | $function$; 12 | -------------------------------------------------------------------------------- /src/Adapters/Storage/Postgres/sql/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var QueryFile = require('pg-promise').QueryFile; 4 | var path = require('path'); 5 | 6 | module.exports = { 7 | array: { 8 | add: sql('array/add.sql'), 9 | addUnique: sql('array/add-unique.sql'), 10 | contains: sql('array/contains.sql'), 11 | containsAll: sql('array/contains-all.sql'), 12 | containsAllRegex: sql('array/contains-all-regex.sql'), 13 | remove: sql('array/remove.sql'), 14 | }, 15 | misc: { 16 | jsonObjectSetKeys: sql('misc/json-object-set-keys.sql'), 17 | }, 18 | }; 19 | 20 | /////////////////////////////////////////////// 21 | // Helper for linking to external query files; 22 | function sql(file) { 23 | var fullPath = path.join(__dirname, file); // generating full path; 24 | 25 | var qf = new QueryFile(fullPath, { minify: true }); 26 | 27 | if (qf.error) { 28 | throw qf.error; 29 | } 30 | 31 | return qf; 32 | } 33 | -------------------------------------------------------------------------------- /src/Adapters/Storage/Postgres/sql/misc/json-object-set-keys.sql: -------------------------------------------------------------------------------- 1 | -- Function to set a key on a nested JSON document 2 | 3 | CREATE OR REPLACE FUNCTION json_object_set_key( 4 | "json" jsonb, 5 | key_to_set TEXT, 6 | value_to_set anyelement 7 | ) 8 | RETURNS jsonb 9 | LANGUAGE sql 10 | IMMUTABLE 11 | STRICT 12 | AS $function$ 13 | SELECT concat('{', string_agg(to_json("key") || ':' || "value", ','), '}')::jsonb 14 | FROM (SELECT * 15 | FROM jsonb_each("json") 16 | WHERE key <> key_to_set 17 | UNION ALL 18 | SELECT key_to_set, to_json("value_to_set")::jsonb) AS fields 19 | $function$; 20 | -------------------------------------------------------------------------------- /src/Adapters/WebSocketServer/WSAdapter.js: -------------------------------------------------------------------------------- 1 | /*eslint no-unused-vars: "off"*/ 2 | import { WSSAdapter } from './WSSAdapter'; 3 | const WebSocketServer = require('ws').Server; 4 | 5 | /** 6 | * Wrapper for ws node module 7 | */ 8 | export class WSAdapter extends WSSAdapter { 9 | constructor(options: any) { 10 | super(options); 11 | this.options = options; 12 | } 13 | 14 | onListen() {} 15 | onConnection(ws) {} 16 | onError(error) {} 17 | start() { 18 | const wss = new WebSocketServer({ server: this.options.server }); 19 | wss.on('listening', this.onListen); 20 | wss.on('connection', this.onConnection); 21 | wss.on('error', this.onError); 22 | } 23 | close() {} 24 | } 25 | 26 | export default WSAdapter; 27 | -------------------------------------------------------------------------------- /src/Adapters/WebSocketServer/WSSAdapter.js: -------------------------------------------------------------------------------- 1 | /*eslint no-unused-vars: "off"*/ 2 | // WebSocketServer Adapter 3 | // 4 | // Adapter classes must implement the following functions: 5 | // * onListen() 6 | // * onConnection(ws) 7 | // * onError(error) 8 | // * start() 9 | // * close() 10 | // 11 | // Default is WSAdapter. The above functions will be binded. 12 | 13 | /** 14 | * @interface 15 | * @memberof module:Adapters 16 | */ 17 | export class WSSAdapter { 18 | /** 19 | * @param {Object} options - {http.Server|https.Server} server 20 | */ 21 | constructor(options) { 22 | this.onListen = () => {}; 23 | this.onConnection = () => {}; 24 | this.onError = () => {}; 25 | } 26 | 27 | // /** 28 | // * Emitted when the underlying server has been bound. 29 | // */ 30 | // onListen() {} 31 | 32 | // /** 33 | // * Emitted when the handshake is complete. 34 | // * 35 | // * @param {WebSocket} ws - RFC 6455 WebSocket. 36 | // */ 37 | // onConnection(ws) {} 38 | 39 | // /** 40 | // * Emitted when error event is called. 41 | // * 42 | // * @param {Error} error - WebSocketServer error 43 | // */ 44 | // onError(error) {} 45 | 46 | /** 47 | * Initialize Connection. 48 | * 49 | * @param {Object} options 50 | */ 51 | start(options) {} 52 | 53 | /** 54 | * Closes server. 55 | */ 56 | close() {} 57 | } 58 | 59 | export default WSSAdapter; 60 | -------------------------------------------------------------------------------- /src/ClientSDK.js: -------------------------------------------------------------------------------- 1 | var semver = require('semver'); 2 | 3 | function compatible(compatibleSDK) { 4 | return function (clientSDK) { 5 | if (typeof clientSDK === 'string') { 6 | clientSDK = fromString(clientSDK); 7 | } 8 | // REST API, or custom SDK 9 | if (!clientSDK) { 10 | return true; 11 | } 12 | const clientVersion = clientSDK.version; 13 | const compatiblityVersion = compatibleSDK[clientSDK.sdk]; 14 | return semver.satisfies(clientVersion, compatiblityVersion); 15 | }; 16 | } 17 | 18 | function supportsForwardDelete(clientSDK) { 19 | return compatible({ 20 | js: '>=1.9.0', 21 | })(clientSDK); 22 | } 23 | 24 | function fromString(version) { 25 | const versionRE = /([-a-zA-Z]+)([0-9\.]+)/; 26 | const match = version.toLowerCase().match(versionRE); 27 | if (match && match.length === 3) { 28 | return { 29 | sdk: match[1], 30 | version: match[2], 31 | }; 32 | } 33 | return undefined; 34 | } 35 | 36 | module.exports = { 37 | compatible, 38 | supportsForwardDelete, 39 | fromString, 40 | }; 41 | -------------------------------------------------------------------------------- /src/Controllers/AdaptableController.js: -------------------------------------------------------------------------------- 1 | /* 2 | AdaptableController.js 3 | 4 | AdaptableController is the base class for all controllers 5 | that support adapter, 6 | The super class takes care of creating the right instance for the adapter 7 | based on the parameters passed 8 | 9 | */ 10 | 11 | // _adapter is private, use Symbol 12 | var _adapter = Symbol(); 13 | 14 | export class AdaptableController { 15 | constructor(adapter, appId, options) { 16 | this.options = options; 17 | this.appId = appId; 18 | this.adapter = adapter; 19 | } 20 | 21 | set adapter(adapter) { 22 | this.validateAdapter(adapter); 23 | this[_adapter] = adapter; 24 | } 25 | 26 | get adapter() { 27 | return this[_adapter]; 28 | } 29 | 30 | expectedAdapterType() { 31 | throw new Error('Subclasses should implement expectedAdapterType()'); 32 | } 33 | 34 | validateAdapter(adapter) { 35 | AdaptableController.validateAdapter(adapter, this); 36 | } 37 | 38 | static validateAdapter(adapter, self, ExpectedType) { 39 | if (!adapter) { 40 | throw new Error(this.constructor.name + ' requires an adapter'); 41 | } 42 | 43 | const Type = ExpectedType || self.expectedAdapterType(); 44 | // Allow skipping for testing 45 | if (!Type) { 46 | return; 47 | } 48 | 49 | // Makes sure the prototype matches 50 | const mismatches = Object.getOwnPropertyNames(Type.prototype).reduce((obj, key) => { 51 | const adapterType = typeof adapter[key]; 52 | const expectedType = typeof Type.prototype[key]; 53 | if (adapterType !== expectedType) { 54 | obj[key] = { 55 | expected: expectedType, 56 | actual: adapterType, 57 | }; 58 | } 59 | return obj; 60 | }, {}); 61 | 62 | if (Object.keys(mismatches).length > 0) { 63 | throw new Error("Adapter prototype don't match expected prototype", adapter, mismatches); 64 | } 65 | } 66 | } 67 | 68 | export default AdaptableController; 69 | -------------------------------------------------------------------------------- /src/Controllers/AnalyticsController.js: -------------------------------------------------------------------------------- 1 | import AdaptableController from './AdaptableController'; 2 | import { AnalyticsAdapter } from '../Adapters/Analytics/AnalyticsAdapter'; 3 | 4 | export class AnalyticsController extends AdaptableController { 5 | appOpened(req) { 6 | return Promise.resolve() 7 | .then(() => { 8 | return this.adapter.appOpened(req.body || {}, req); 9 | }) 10 | .then(response => { 11 | return { response: response || {} }; 12 | }) 13 | .catch(() => { 14 | return { response: {} }; 15 | }); 16 | } 17 | 18 | trackEvent(req) { 19 | return Promise.resolve() 20 | .then(() => { 21 | return this.adapter.trackEvent(req.params.eventName, req.body || {}, req); 22 | }) 23 | .then(response => { 24 | return { response: response || {} }; 25 | }) 26 | .catch(() => { 27 | return { response: {} }; 28 | }); 29 | } 30 | 31 | expectedAdapterType() { 32 | return AnalyticsAdapter; 33 | } 34 | } 35 | 36 | export default AnalyticsController; 37 | -------------------------------------------------------------------------------- /src/Controllers/CacheController.js: -------------------------------------------------------------------------------- 1 | import AdaptableController from './AdaptableController'; 2 | import CacheAdapter from '../Adapters/Cache/CacheAdapter'; 3 | 4 | const KEY_SEPARATOR_CHAR = ':'; 5 | 6 | function joinKeys(...keys) { 7 | return keys.join(KEY_SEPARATOR_CHAR); 8 | } 9 | 10 | /** 11 | * Prefix all calls to the cache via a prefix string, useful when grouping Cache by object type. 12 | * 13 | * eg "Role" or "Session" 14 | */ 15 | export class SubCache { 16 | constructor(prefix, cacheController, ttl) { 17 | this.prefix = prefix; 18 | this.cache = cacheController; 19 | this.ttl = ttl; 20 | } 21 | 22 | get(key) { 23 | const cacheKey = joinKeys(this.prefix, key); 24 | return this.cache.get(cacheKey); 25 | } 26 | 27 | put(key, value, ttl) { 28 | const cacheKey = joinKeys(this.prefix, key); 29 | return this.cache.put(cacheKey, value, ttl); 30 | } 31 | 32 | del(key) { 33 | const cacheKey = joinKeys(this.prefix, key); 34 | return this.cache.del(cacheKey); 35 | } 36 | 37 | clear() { 38 | return this.cache.clear(); 39 | } 40 | } 41 | 42 | export class CacheController extends AdaptableController { 43 | constructor(adapter, appId, options = {}) { 44 | super(adapter, appId, options); 45 | 46 | this.role = new SubCache('role', this); 47 | this.user = new SubCache('user', this); 48 | this.graphQL = new SubCache('graphQL', this); 49 | } 50 | 51 | get(key) { 52 | const cacheKey = joinKeys(this.appId, key); 53 | return this.adapter.get(cacheKey).then(null, () => Promise.resolve(null)); 54 | } 55 | 56 | put(key, value, ttl) { 57 | const cacheKey = joinKeys(this.appId, key); 58 | return this.adapter.put(cacheKey, value, ttl); 59 | } 60 | 61 | del(key) { 62 | const cacheKey = joinKeys(this.appId, key); 63 | return this.adapter.del(cacheKey); 64 | } 65 | 66 | clear() { 67 | return this.adapter.clear(); 68 | } 69 | 70 | expectedAdapterType() { 71 | return CacheAdapter; 72 | } 73 | } 74 | 75 | export default CacheController; 76 | -------------------------------------------------------------------------------- /src/Controllers/types.js: -------------------------------------------------------------------------------- 1 | export type LoadSchemaOptions = { 2 | clearCache: boolean, 3 | }; 4 | 5 | export type SchemaField = { 6 | type: string, 7 | targetClass?: ?string, 8 | required?: ?boolean, 9 | defaultValue?: ?any, 10 | }; 11 | 12 | export type SchemaFields = { [string]: SchemaField }; 13 | 14 | export type Schema = { 15 | className: string, 16 | fields: SchemaFields, 17 | classLevelPermissions: ClassLevelPermissions, 18 | indexes?: ?any, 19 | }; 20 | 21 | export type ClassLevelPermissions = { 22 | ACL?: { 23 | [string]: { 24 | [string]: boolean, 25 | }, 26 | }, 27 | find?: { [string]: boolean }, 28 | count?: { [string]: boolean }, 29 | get?: { [string]: boolean }, 30 | create?: { [string]: boolean }, 31 | update?: { [string]: boolean }, 32 | delete?: { [string]: boolean }, 33 | addField?: { [string]: boolean }, 34 | readUserFields?: string[], 35 | writeUserFields?: string[], 36 | protectedFields?: { [string]: string[] }, 37 | }; 38 | -------------------------------------------------------------------------------- /src/Deprecator/Deprecations.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The deprecations. 3 | * 4 | * Add deprecations to the array using the following keys: 5 | * - `optionKey` {String}: The option key incl. its path, e.g. `security.enableCheck`. 6 | * - `envKey` {String}: The environment key, e.g. `PARSE_SERVER_SECURITY`. 7 | * - `changeNewKey` {String}: Set the new key name if the current key will be replaced, 8 | * or set to an empty string if the current key will be removed without replacement. 9 | * - `changeNewDefault` {String}: Set the new default value if the key's default value 10 | * will change in a future version. 11 | * - `solution`: The instruction to resolve this deprecation warning. Optional. This 12 | * instruction must not include the deprecation warning which is auto-generated. 13 | * It should only contain additional instruction regarding the deprecation if 14 | * necessary. 15 | * 16 | * If there are no deprecations, this must return an empty array. 17 | */ 18 | module.exports = [ 19 | { optionKey: 'encodeParseObjectInCloudFunction', changeNewDefault: 'true' }, 20 | { optionKey: 'enableInsecureAuthAdapters', changeNewDefault: 'false' }, 21 | ]; 22 | -------------------------------------------------------------------------------- /src/GraphQL/helpers/objectsMutations.js: -------------------------------------------------------------------------------- 1 | import rest from '../../rest'; 2 | 3 | const createObject = async (className, fields, config, auth, info) => { 4 | if (!fields) { 5 | fields = {}; 6 | } 7 | 8 | return (await rest.create(config, auth, className, fields, info.clientSDK, info.context)) 9 | .response; 10 | }; 11 | 12 | const updateObject = async (className, objectId, fields, config, auth, info) => { 13 | if (!fields) { 14 | fields = {}; 15 | } 16 | 17 | return ( 18 | await rest.update(config, auth, className, { objectId }, fields, info.clientSDK, info.context) 19 | ).response; 20 | }; 21 | 22 | const deleteObject = async (className, objectId, config, auth, info) => { 23 | await rest.del(config, auth, className, objectId, info.context); 24 | return true; 25 | }; 26 | 27 | export { createObject, updateObject, deleteObject }; 28 | -------------------------------------------------------------------------------- /src/GraphQL/loaders/defaultGraphQLMutations.js: -------------------------------------------------------------------------------- 1 | import * as filesMutations from './filesMutations'; 2 | import * as usersMutations from './usersMutations'; 3 | import * as functionsMutations from './functionsMutations'; 4 | import * as schemaMutations from './schemaMutations'; 5 | 6 | const load = parseGraphQLSchema => { 7 | filesMutations.load(parseGraphQLSchema); 8 | usersMutations.load(parseGraphQLSchema); 9 | functionsMutations.load(parseGraphQLSchema); 10 | schemaMutations.load(parseGraphQLSchema); 11 | }; 12 | 13 | export { load }; 14 | -------------------------------------------------------------------------------- /src/GraphQL/loaders/defaultGraphQLQueries.js: -------------------------------------------------------------------------------- 1 | import { GraphQLNonNull, GraphQLBoolean } from 'graphql'; 2 | import * as usersQueries from './usersQueries'; 3 | import * as schemaQueries from './schemaQueries'; 4 | 5 | const load = parseGraphQLSchema => { 6 | parseGraphQLSchema.addGraphQLQuery( 7 | 'health', 8 | { 9 | description: 'The health query can be used to check if the server is up and running.', 10 | type: new GraphQLNonNull(GraphQLBoolean), 11 | resolve: () => true, 12 | }, 13 | true, 14 | true 15 | ); 16 | 17 | usersQueries.load(parseGraphQLSchema); 18 | schemaQueries.load(parseGraphQLSchema); 19 | }; 20 | 21 | export { load }; 22 | -------------------------------------------------------------------------------- /src/GraphQL/loaders/defaultRelaySchema.js: -------------------------------------------------------------------------------- 1 | import { nodeDefinitions, fromGlobalId } from 'graphql-relay'; 2 | import getFieldNames from 'graphql-list-fields'; 3 | import * as defaultGraphQLTypes from './defaultGraphQLTypes'; 4 | import * as objectsQueries from '../helpers/objectsQueries'; 5 | import { extractKeysAndInclude } from './parseClassTypes'; 6 | 7 | const GLOBAL_ID_ATT = { 8 | description: 'This is the global id.', 9 | type: defaultGraphQLTypes.OBJECT_ID, 10 | }; 11 | 12 | const load = parseGraphQLSchema => { 13 | const { nodeInterface, nodeField } = nodeDefinitions( 14 | async (globalId, context, queryInfo) => { 15 | try { 16 | const { type, id } = fromGlobalId(globalId); 17 | const { config, auth, info } = context; 18 | const selectedFields = getFieldNames(queryInfo); 19 | 20 | const { keys, include } = extractKeysAndInclude(selectedFields); 21 | 22 | return { 23 | className: type, 24 | ...(await objectsQueries.getObject( 25 | type, 26 | id, 27 | keys, 28 | include, 29 | undefined, 30 | undefined, 31 | config, 32 | auth, 33 | info, 34 | parseGraphQLSchema.parseClasses 35 | )), 36 | }; 37 | } catch (e) { 38 | parseGraphQLSchema.handleError(e); 39 | } 40 | }, 41 | obj => { 42 | return parseGraphQLSchema.parseClassTypes[obj.className].classGraphQLOutputType.name; 43 | } 44 | ); 45 | 46 | parseGraphQLSchema.addGraphQLType(nodeInterface, true); 47 | parseGraphQLSchema.relayNodeInterface = nodeInterface; 48 | parseGraphQLSchema.addGraphQLQuery('node', nodeField, true); 49 | }; 50 | 51 | export { GLOBAL_ID_ATT, load }; 52 | -------------------------------------------------------------------------------- /src/GraphQL/loaders/schemaDirectives.js: -------------------------------------------------------------------------------- 1 | import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils'; 2 | import { FunctionsRouter } from '../../Routers/FunctionsRouter'; 3 | 4 | export const definitions = ` 5 | directive @resolve(to: String) on FIELD_DEFINITION 6 | directive @mock(with: Any!) on FIELD_DEFINITION 7 | `; 8 | 9 | const load = parseGraphQLSchema => { 10 | parseGraphQLSchema.graphQLSchemaDirectivesDefinitions = definitions; 11 | 12 | const resolveDirective = schema => 13 | mapSchema(schema, { 14 | [MapperKind.OBJECT_FIELD]: fieldConfig => { 15 | const directive = getDirective(schema, fieldConfig, 'resolve')?.[0]; 16 | if (directive) { 17 | const { to: targetCloudFunction } = directive; 18 | fieldConfig.resolve = async (_source, args, context, gqlInfo) => { 19 | try { 20 | const { config, auth, info } = context; 21 | const functionName = targetCloudFunction || gqlInfo.fieldName; 22 | return ( 23 | await FunctionsRouter.handleCloudFunction({ 24 | params: { 25 | functionName, 26 | }, 27 | config, 28 | auth, 29 | info, 30 | body: args, 31 | }) 32 | ).response.result; 33 | } catch (e) { 34 | parseGraphQLSchema.handleError(e); 35 | } 36 | }; 37 | } 38 | return fieldConfig; 39 | }, 40 | }); 41 | 42 | const mockDirective = schema => 43 | mapSchema(schema, { 44 | [MapperKind.OBJECT_FIELD]: fieldConfig => { 45 | const directive = getDirective(schema, fieldConfig, 'mock')?.[0]; 46 | if (directive) { 47 | const { with: mockValue } = directive; 48 | fieldConfig.resolve = async () => mockValue; 49 | } 50 | return fieldConfig; 51 | }, 52 | }); 53 | 54 | parseGraphQLSchema.graphQLSchemaDirectives = schema => mockDirective(resolveDirective(schema)); 55 | }; 56 | export { load }; 57 | -------------------------------------------------------------------------------- /src/GraphQL/parseGraphQLUtils.js: -------------------------------------------------------------------------------- 1 | import Parse from 'parse/node'; 2 | import { GraphQLError } from 'graphql'; 3 | 4 | export function enforceMasterKeyAccess(auth) { 5 | if (!auth.isMaster) { 6 | throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'unauthorized: master key is required'); 7 | } 8 | } 9 | 10 | export function toGraphQLError(error) { 11 | let code, message; 12 | if (error instanceof Parse.Error) { 13 | code = error.code; 14 | message = error.message; 15 | } else { 16 | code = Parse.Error.INTERNAL_SERVER_ERROR; 17 | message = 'Internal server error'; 18 | } 19 | return new GraphQLError(message, { extensions: { code } }); 20 | } 21 | 22 | export const extractKeysAndInclude = selectedFields => { 23 | selectedFields = selectedFields.filter(field => !field.includes('__typename')); 24 | // Handles "id" field for both current and included objects 25 | selectedFields = selectedFields.map(field => { 26 | if (field === 'id') { return 'objectId'; } 27 | return field.endsWith('.id') 28 | ? `${field.substring(0, field.lastIndexOf('.id'))}.objectId` 29 | : field; 30 | }); 31 | let keys = undefined; 32 | let include = undefined; 33 | 34 | if (selectedFields.length > 0) { 35 | keys = [...new Set(selectedFields)].join(','); 36 | // We can use this shortcut since optimization is handled 37 | // later on RestQuery, avoid overhead here. 38 | include = keys; 39 | } 40 | 41 | return { 42 | // If authData is detected keys will not work properly 43 | // since authData has a special storage behavior 44 | // so we need to skip keys currently 45 | keys: keys && keys.indexOf('authData') === -1 ? keys : undefined, 46 | include, 47 | }; 48 | }; 49 | 50 | export const getParseClassMutationConfig = function (parseClassConfig) { 51 | return (parseClassConfig && parseClassConfig.mutation) || {}; 52 | }; 53 | -------------------------------------------------------------------------------- /src/GraphQL/transformers/className.js: -------------------------------------------------------------------------------- 1 | const transformClassNameToGraphQL = className => { 2 | if (className[0] === '_') { 3 | className = className.slice(1); 4 | } 5 | return className[0].toUpperCase() + className.slice(1); 6 | }; 7 | 8 | export { transformClassNameToGraphQL }; 9 | -------------------------------------------------------------------------------- /src/GraphQL/transformers/constraintType.js: -------------------------------------------------------------------------------- 1 | import * as defaultGraphQLTypes from '../loaders/defaultGraphQLTypes'; 2 | 3 | const transformConstraintTypeToGraphQL = (parseType, targetClass, parseClassTypes, fieldName) => { 4 | if (fieldName === 'id' || fieldName === 'objectId') { 5 | return defaultGraphQLTypes.ID_WHERE_INPUT; 6 | } 7 | 8 | switch (parseType) { 9 | case 'String': 10 | return defaultGraphQLTypes.STRING_WHERE_INPUT; 11 | case 'Number': 12 | return defaultGraphQLTypes.NUMBER_WHERE_INPUT; 13 | case 'Boolean': 14 | return defaultGraphQLTypes.BOOLEAN_WHERE_INPUT; 15 | case 'Array': 16 | return defaultGraphQLTypes.ARRAY_WHERE_INPUT; 17 | case 'Object': 18 | return defaultGraphQLTypes.OBJECT_WHERE_INPUT; 19 | case 'Date': 20 | return defaultGraphQLTypes.DATE_WHERE_INPUT; 21 | case 'Pointer': 22 | if ( 23 | parseClassTypes[targetClass] && 24 | parseClassTypes[targetClass].classGraphQLRelationConstraintsType 25 | ) { 26 | return parseClassTypes[targetClass].classGraphQLRelationConstraintsType; 27 | } else { 28 | return defaultGraphQLTypes.OBJECT; 29 | } 30 | case 'File': 31 | return defaultGraphQLTypes.FILE_WHERE_INPUT; 32 | case 'GeoPoint': 33 | return defaultGraphQLTypes.GEO_POINT_WHERE_INPUT; 34 | case 'Polygon': 35 | return defaultGraphQLTypes.POLYGON_WHERE_INPUT; 36 | case 'Bytes': 37 | return defaultGraphQLTypes.BYTES_WHERE_INPUT; 38 | case 'ACL': 39 | return defaultGraphQLTypes.OBJECT_WHERE_INPUT; 40 | case 'Relation': 41 | if ( 42 | parseClassTypes[targetClass] && 43 | parseClassTypes[targetClass].classGraphQLRelationConstraintsType 44 | ) { 45 | return parseClassTypes[targetClass].classGraphQLRelationConstraintsType; 46 | } else { 47 | return defaultGraphQLTypes.OBJECT; 48 | } 49 | default: 50 | return undefined; 51 | } 52 | }; 53 | 54 | export { transformConstraintTypeToGraphQL }; 55 | -------------------------------------------------------------------------------- /src/GraphQL/transformers/inputType.js: -------------------------------------------------------------------------------- 1 | import { GraphQLString, GraphQLFloat, GraphQLBoolean, GraphQLList } from 'graphql'; 2 | import * as defaultGraphQLTypes from '../loaders/defaultGraphQLTypes'; 3 | 4 | const transformInputTypeToGraphQL = (parseType, targetClass, parseClassTypes) => { 5 | switch (parseType) { 6 | case 'String': 7 | return GraphQLString; 8 | case 'Number': 9 | return GraphQLFloat; 10 | case 'Boolean': 11 | return GraphQLBoolean; 12 | case 'Array': 13 | return new GraphQLList(defaultGraphQLTypes.ANY); 14 | case 'Object': 15 | return defaultGraphQLTypes.OBJECT; 16 | case 'Date': 17 | return defaultGraphQLTypes.DATE; 18 | case 'Pointer': 19 | if ( 20 | parseClassTypes && 21 | parseClassTypes[targetClass] && 22 | parseClassTypes[targetClass].classGraphQLPointerType 23 | ) { 24 | return parseClassTypes[targetClass].classGraphQLPointerType; 25 | } else { 26 | return defaultGraphQLTypes.OBJECT; 27 | } 28 | case 'Relation': 29 | if ( 30 | parseClassTypes && 31 | parseClassTypes[targetClass] && 32 | parseClassTypes[targetClass].classGraphQLRelationType 33 | ) { 34 | return parseClassTypes[targetClass].classGraphQLRelationType; 35 | } else { 36 | return defaultGraphQLTypes.OBJECT; 37 | } 38 | case 'File': 39 | return defaultGraphQLTypes.FILE_INPUT; 40 | case 'GeoPoint': 41 | return defaultGraphQLTypes.GEO_POINT_INPUT; 42 | case 'Polygon': 43 | return defaultGraphQLTypes.POLYGON_INPUT; 44 | case 'Bytes': 45 | return defaultGraphQLTypes.BYTES; 46 | case 'ACL': 47 | return defaultGraphQLTypes.ACL_INPUT; 48 | default: 49 | return undefined; 50 | } 51 | }; 52 | 53 | export { transformInputTypeToGraphQL }; 54 | -------------------------------------------------------------------------------- /src/GraphQL/transformers/outputType.js: -------------------------------------------------------------------------------- 1 | import * as defaultGraphQLTypes from '../loaders/defaultGraphQLTypes'; 2 | import { GraphQLString, GraphQLFloat, GraphQLBoolean, GraphQLList, GraphQLNonNull } from 'graphql'; 3 | 4 | const transformOutputTypeToGraphQL = (parseType, targetClass, parseClassTypes) => { 5 | switch (parseType) { 6 | case 'String': 7 | return GraphQLString; 8 | case 'Number': 9 | return GraphQLFloat; 10 | case 'Boolean': 11 | return GraphQLBoolean; 12 | case 'Array': 13 | return new GraphQLList(defaultGraphQLTypes.ARRAY_RESULT); 14 | case 'Object': 15 | return defaultGraphQLTypes.OBJECT; 16 | case 'Date': 17 | return defaultGraphQLTypes.DATE; 18 | case 'Pointer': 19 | if ( 20 | parseClassTypes && 21 | parseClassTypes[targetClass] && 22 | parseClassTypes[targetClass].classGraphQLOutputType 23 | ) { 24 | return parseClassTypes[targetClass].classGraphQLOutputType; 25 | } else { 26 | return defaultGraphQLTypes.OBJECT; 27 | } 28 | case 'Relation': 29 | if ( 30 | parseClassTypes && 31 | parseClassTypes[targetClass] && 32 | parseClassTypes[targetClass].classGraphQLFindResultType 33 | ) { 34 | return new GraphQLNonNull(parseClassTypes[targetClass].classGraphQLFindResultType); 35 | } else { 36 | return new GraphQLNonNull(defaultGraphQLTypes.OBJECT); 37 | } 38 | case 'File': 39 | return defaultGraphQLTypes.FILE_INFO; 40 | case 'GeoPoint': 41 | return defaultGraphQLTypes.GEO_POINT; 42 | case 'Polygon': 43 | return defaultGraphQLTypes.POLYGON; 44 | case 'Bytes': 45 | return defaultGraphQLTypes.BYTES; 46 | case 'ACL': 47 | return new GraphQLNonNull(defaultGraphQLTypes.ACL); 48 | default: 49 | return undefined; 50 | } 51 | }; 52 | 53 | export { transformOutputTypeToGraphQL }; 54 | -------------------------------------------------------------------------------- /src/KeyPromiseQueue.js: -------------------------------------------------------------------------------- 1 | // KeyPromiseQueue is a simple promise queue 2 | // used to queue operations per key basis. 3 | // Once the tail promise in the key-queue fulfills, 4 | // the chain on that key will be cleared. 5 | export class KeyPromiseQueue { 6 | constructor() { 7 | this.queue = {}; 8 | } 9 | 10 | enqueue(key, operation) { 11 | const tuple = this.beforeOp(key); 12 | const toAwait = tuple[1]; 13 | const nextOperation = toAwait.then(operation); 14 | const wrappedOperation = nextOperation.then(result => { 15 | this.afterOp(key); 16 | return result; 17 | }); 18 | tuple[1] = wrappedOperation; 19 | return wrappedOperation; 20 | } 21 | 22 | beforeOp(key) { 23 | let tuple = this.queue[key]; 24 | if (!tuple) { 25 | tuple = [0, Promise.resolve()]; 26 | this.queue[key] = tuple; 27 | } 28 | tuple[0]++; 29 | return tuple; 30 | } 31 | 32 | afterOp(key) { 33 | const tuple = this.queue[key]; 34 | if (!tuple) { 35 | return; 36 | } 37 | tuple[0]--; 38 | if (tuple[0] <= 0) { 39 | delete this.queue[key]; 40 | return; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/LiveQuery/Id.js: -------------------------------------------------------------------------------- 1 | class Id { 2 | className: string; 3 | objectId: string; 4 | 5 | constructor(className: string, objectId: string) { 6 | this.className = className; 7 | this.objectId = objectId; 8 | } 9 | toString(): string { 10 | return this.className + ':' + this.objectId; 11 | } 12 | 13 | static fromString(str: string) { 14 | var split = str.split(':'); 15 | if (split.length !== 2) { 16 | throw new TypeError('Cannot create Id object from this string'); 17 | } 18 | return new Id(split[0], split[1]); 19 | } 20 | } 21 | 22 | module.exports = Id; 23 | -------------------------------------------------------------------------------- /src/LiveQuery/ParseCloudCodePublisher.js: -------------------------------------------------------------------------------- 1 | import { ParsePubSub } from './ParsePubSub'; 2 | import Parse from 'parse/node'; 3 | import logger from '../logger'; 4 | 5 | class ParseCloudCodePublisher { 6 | parsePublisher: Object; 7 | 8 | // config object of the publisher, right now it only contains the redisURL, 9 | // but we may extend it later. 10 | constructor(config: any = {}) { 11 | this.parsePublisher = ParsePubSub.createPublisher(config); 12 | } 13 | 14 | async connect() { 15 | if (typeof this.parsePublisher.connect === 'function') { 16 | if (this.parsePublisher.isOpen) { 17 | return; 18 | } 19 | return Promise.resolve(this.parsePublisher.connect()); 20 | } 21 | } 22 | 23 | onCloudCodeAfterSave(request: any): void { 24 | this._onCloudCodeMessage(Parse.applicationId + 'afterSave', request); 25 | } 26 | 27 | onCloudCodeAfterDelete(request: any): void { 28 | this._onCloudCodeMessage(Parse.applicationId + 'afterDelete', request); 29 | } 30 | 31 | onClearCachedRoles(user: Parse.Object) { 32 | this.parsePublisher.publish( 33 | Parse.applicationId + 'clearCache', 34 | JSON.stringify({ userId: user.id }) 35 | ); 36 | } 37 | 38 | // Request is the request object from cloud code functions. request.object is a ParseObject. 39 | _onCloudCodeMessage(type: string, request: any): void { 40 | logger.verbose( 41 | 'Raw request from cloud code current : %j | original : %j', 42 | request.object, 43 | request.original 44 | ); 45 | // We need the full JSON which includes className 46 | const message = { 47 | currentParseObject: request.object._toFullJSON(), 48 | }; 49 | if (request.original) { 50 | message.originalParseObject = request.original._toFullJSON(); 51 | } 52 | if (request.classLevelPermissions) { 53 | message.classLevelPermissions = request.classLevelPermissions; 54 | } 55 | this.parsePublisher.publish(type, JSON.stringify(message)); 56 | } 57 | } 58 | 59 | export { ParseCloudCodePublisher }; 60 | -------------------------------------------------------------------------------- /src/LiveQuery/ParsePubSub.js: -------------------------------------------------------------------------------- 1 | import { loadAdapter } from '../Adapters/AdapterLoader'; 2 | import { EventEmitterPubSub } from '../Adapters/PubSub/EventEmitterPubSub'; 3 | 4 | import { RedisPubSub } from '../Adapters/PubSub/RedisPubSub'; 5 | 6 | const ParsePubSub = {}; 7 | 8 | function useRedis(config: any): boolean { 9 | const redisURL = config.redisURL; 10 | return typeof redisURL !== 'undefined' && redisURL !== ''; 11 | } 12 | 13 | ParsePubSub.createPublisher = function (config: any): any { 14 | if (useRedis(config)) { 15 | return RedisPubSub.createPublisher(config); 16 | } else { 17 | const adapter = loadAdapter(config.pubSubAdapter, EventEmitterPubSub, config); 18 | if (typeof adapter.createPublisher !== 'function') { 19 | throw 'pubSubAdapter should have createPublisher()'; 20 | } 21 | return adapter.createPublisher(config); 22 | } 23 | }; 24 | 25 | ParsePubSub.createSubscriber = function (config: any): void { 26 | if (useRedis(config)) { 27 | return RedisPubSub.createSubscriber(config); 28 | } else { 29 | const adapter = loadAdapter(config.pubSubAdapter, EventEmitterPubSub, config); 30 | if (typeof adapter.createSubscriber !== 'function') { 31 | throw 'pubSubAdapter should have createSubscriber()'; 32 | } 33 | return adapter.createSubscriber(config); 34 | } 35 | }; 36 | 37 | export { ParsePubSub }; 38 | -------------------------------------------------------------------------------- /src/LiveQuery/ParseWebSocketServer.js: -------------------------------------------------------------------------------- 1 | import { loadAdapter } from '../Adapters/AdapterLoader'; 2 | import { WSAdapter } from '../Adapters/WebSocketServer/WSAdapter'; 3 | import logger from '../logger'; 4 | import events from 'events'; 5 | import { inspect } from 'util'; 6 | 7 | export class ParseWebSocketServer { 8 | server: Object; 9 | 10 | constructor(server: any, onConnect: Function, config) { 11 | config.server = server; 12 | const wss = loadAdapter(config.wssAdapter, WSAdapter, config); 13 | wss.onListen = () => { 14 | logger.info('Parse LiveQuery Server started running'); 15 | }; 16 | wss.onConnection = ws => { 17 | ws.waitingForPong = false; 18 | ws.on('pong', () => { 19 | ws.waitingForPong = false; 20 | }); 21 | ws.on('error', error => { 22 | logger.error(error.message); 23 | logger.error(inspect(ws, false)); 24 | }); 25 | onConnect(new ParseWebSocket(ws)); 26 | // Send ping to client periodically 27 | const pingIntervalId = setInterval(() => { 28 | if (!ws.waitingForPong) { 29 | ws.ping(); 30 | ws.waitingForPong = true; 31 | } else { 32 | clearInterval(pingIntervalId); 33 | ws.terminate(); 34 | } 35 | }, config.websocketTimeout || 10 * 1000); 36 | }; 37 | wss.onError = error => { 38 | logger.error(error); 39 | }; 40 | wss.start(); 41 | this.server = wss; 42 | } 43 | 44 | close() { 45 | if (this.server && this.server.close) { 46 | this.server.close(); 47 | } 48 | } 49 | } 50 | 51 | export class ParseWebSocket extends events.EventEmitter { 52 | ws: any; 53 | 54 | constructor(ws: any) { 55 | super(); 56 | ws.onmessage = request => 57 | this.emit('message', request && request.data ? request.data : request); 58 | ws.onclose = () => this.emit('disconnect'); 59 | this.ws = ws; 60 | } 61 | 62 | send(message: any): void { 63 | this.ws.send(message); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/LiveQuery/SessionTokenCache.js: -------------------------------------------------------------------------------- 1 | import Parse from 'parse/node'; 2 | import { LRUCache as LRU } from 'lru-cache'; 3 | import logger from '../logger'; 4 | 5 | function userForSessionToken(sessionToken) { 6 | var q = new Parse.Query('_Session'); 7 | q.equalTo('sessionToken', sessionToken); 8 | return q.first({ useMasterKey: true }).then(function (session) { 9 | if (!session) { 10 | return Promise.reject('No session found for session token'); 11 | } 12 | return session.get('user'); 13 | }); 14 | } 15 | 16 | class SessionTokenCache { 17 | cache: Object; 18 | 19 | constructor(timeout: number = 30 * 24 * 60 * 60 * 1000, maxSize: number = 10000) { 20 | this.cache = new LRU({ 21 | max: maxSize, 22 | ttl: timeout, 23 | }); 24 | } 25 | 26 | getUserId(sessionToken: string): any { 27 | if (!sessionToken) { 28 | return Promise.reject('Empty sessionToken'); 29 | } 30 | const userId = this.cache.get(sessionToken); 31 | if (userId) { 32 | logger.verbose('Fetch userId %s of sessionToken %s from Cache', userId, sessionToken); 33 | return Promise.resolve(userId); 34 | } 35 | return userForSessionToken(sessionToken).then( 36 | user => { 37 | logger.verbose('Fetch userId %s of sessionToken %s from Parse', user.id, sessionToken); 38 | const userId = user.id; 39 | this.cache.set(sessionToken, userId); 40 | return Promise.resolve(userId); 41 | }, 42 | error => { 43 | logger.error('Can not fetch userId for sessionToken %j, error %j', sessionToken, error); 44 | return Promise.reject(error); 45 | } 46 | ); 47 | } 48 | } 49 | 50 | export { SessionTokenCache }; 51 | -------------------------------------------------------------------------------- /src/LiveQuery/Subscription.js: -------------------------------------------------------------------------------- 1 | import logger from '../logger'; 2 | 3 | export type FlattenedObjectData = { [attr: string]: any }; 4 | export type QueryData = { [attr: string]: any }; 5 | 6 | class Subscription { 7 | // It is query condition eg query.where 8 | query: QueryData; 9 | className: string; 10 | hash: string; 11 | clientRequestIds: Object; 12 | 13 | constructor(className: string, query: QueryData, queryHash: string) { 14 | this.className = className; 15 | this.query = query; 16 | this.hash = queryHash; 17 | this.clientRequestIds = new Map(); 18 | } 19 | 20 | addClientSubscription(clientId: number, requestId: number): void { 21 | if (!this.clientRequestIds.has(clientId)) { 22 | this.clientRequestIds.set(clientId, []); 23 | } 24 | const requestIds = this.clientRequestIds.get(clientId); 25 | requestIds.push(requestId); 26 | } 27 | 28 | deleteClientSubscription(clientId: number, requestId: number): void { 29 | const requestIds = this.clientRequestIds.get(clientId); 30 | if (typeof requestIds === 'undefined') { 31 | logger.error('Can not find client %d to delete', clientId); 32 | return; 33 | } 34 | 35 | const index = requestIds.indexOf(requestId); 36 | if (index < 0) { 37 | logger.error('Can not find client %d subscription %d to delete', clientId, requestId); 38 | return; 39 | } 40 | requestIds.splice(index, 1); 41 | // Delete client reference if it has no subscription 42 | if (requestIds.length == 0) { 43 | this.clientRequestIds.delete(clientId); 44 | } 45 | } 46 | 47 | hasSubscribingClient(): boolean { 48 | return this.clientRequestIds.size > 0; 49 | } 50 | } 51 | 52 | export { Subscription }; 53 | -------------------------------------------------------------------------------- /src/LiveQuery/equalObjects.js: -------------------------------------------------------------------------------- 1 | var toString = Object.prototype.toString; 2 | 3 | /** 4 | * Determines whether two objects represent the same primitive, special Parse 5 | * type, or full Parse Object. 6 | */ 7 | function equalObjects(a, b) { 8 | if (typeof a !== typeof b) { 9 | return false; 10 | } 11 | if (typeof a !== 'object') { 12 | return a === b; 13 | } 14 | if (a === b) { 15 | return true; 16 | } 17 | if (toString.call(a) === '[object Date]') { 18 | if (toString.call(b) === '[object Date]') { 19 | return +a === +b; 20 | } 21 | return false; 22 | } 23 | if (Array.isArray(a)) { 24 | if (Array.isArray(b)) { 25 | if (a.length !== b.length) { 26 | return false; 27 | } 28 | for (var i = 0; i < a.length; i++) { 29 | if (!equalObjects(a[i], b[i])) { 30 | return false; 31 | } 32 | } 33 | return true; 34 | } 35 | return false; 36 | } 37 | if (Object.keys(a).length !== Object.keys(b).length) { 38 | return false; 39 | } 40 | for (var key in a) { 41 | if (!equalObjects(a[key], b[key])) { 42 | return false; 43 | } 44 | } 45 | return true; 46 | } 47 | 48 | module.exports = equalObjects; 49 | -------------------------------------------------------------------------------- /src/Options/parsers.js: -------------------------------------------------------------------------------- 1 | function numberParser(key) { 2 | return function (opt) { 3 | const intOpt = parseInt(opt); 4 | if (!Number.isInteger(intOpt)) { 5 | throw new Error(`Key ${key} has invalid value ${opt}`); 6 | } 7 | return intOpt; 8 | }; 9 | } 10 | 11 | function numberOrBoolParser(key) { 12 | return function (opt) { 13 | if (typeof opt === 'boolean') { 14 | return opt; 15 | } 16 | if (opt === 'true') { 17 | return true; 18 | } 19 | if (opt === 'false') { 20 | return false; 21 | } 22 | return numberParser(key)(opt); 23 | }; 24 | } 25 | 26 | function numberOrStringParser(key) { 27 | return function (opt) { 28 | if (typeof opt === 'string') { 29 | return opt; 30 | } 31 | return numberParser(key)(opt); 32 | }; 33 | } 34 | 35 | function objectParser(opt) { 36 | if (typeof opt == 'object') { 37 | return opt; 38 | } 39 | return JSON.parse(opt); 40 | } 41 | 42 | function arrayParser(opt) { 43 | if (Array.isArray(opt)) { 44 | return opt; 45 | } else if (typeof opt === 'string') { 46 | return opt.split(','); 47 | } else { 48 | throw new Error(`${opt} should be a comma separated string or an array`); 49 | } 50 | } 51 | 52 | function moduleOrObjectParser(opt) { 53 | if (typeof opt == 'object') { 54 | return opt; 55 | } 56 | try { 57 | return JSON.parse(opt); 58 | } catch (e) { 59 | /* */ 60 | } 61 | return opt; 62 | } 63 | 64 | function booleanParser(opt) { 65 | if (opt == true || opt == 'true' || opt == '1') { 66 | return true; 67 | } 68 | return false; 69 | } 70 | 71 | function nullParser(opt) { 72 | if (opt == 'null') { 73 | return null; 74 | } 75 | return opt; 76 | } 77 | 78 | module.exports = { 79 | numberParser, 80 | numberOrBoolParser, 81 | numberOrStringParser, 82 | nullParser, 83 | booleanParser, 84 | moduleOrObjectParser, 85 | arrayParser, 86 | objectParser, 87 | }; 88 | -------------------------------------------------------------------------------- /src/Page.js: -------------------------------------------------------------------------------- 1 | /*eslint no-unused-vars: "off"*/ 2 | /** 3 | * @interface Page 4 | * Page 5 | * Page content that is returned by PageRouter. 6 | */ 7 | export class Page { 8 | /** 9 | * @description Creates a page. 10 | * @param {Object} params The page parameters. 11 | * @param {String} params.id The page identifier. 12 | * @param {String} params.defaultFile The page file name. 13 | * @returns {Page} The page. 14 | */ 15 | constructor(params = {}) { 16 | const { id, defaultFile } = params; 17 | 18 | this._id = id; 19 | this._defaultFile = defaultFile; 20 | } 21 | 22 | get id() { 23 | return this._id; 24 | } 25 | get defaultFile() { 26 | return this._defaultFile; 27 | } 28 | set id(v) { 29 | this._id = v; 30 | } 31 | set defaultFile(v) { 32 | this._defaultFile = v; 33 | } 34 | } 35 | 36 | export default Page; 37 | -------------------------------------------------------------------------------- /src/ParseMessageQueue.js: -------------------------------------------------------------------------------- 1 | import { loadAdapter } from './Adapters/AdapterLoader'; 2 | import { EventEmitterMQ } from './Adapters/MessageQueue/EventEmitterMQ'; 3 | 4 | const ParseMessageQueue = {}; 5 | 6 | ParseMessageQueue.createPublisher = function (config: any): any { 7 | const adapter = loadAdapter(config.messageQueueAdapter, EventEmitterMQ, config); 8 | if (typeof adapter.createPublisher !== 'function') { 9 | throw 'pubSubAdapter should have createPublisher()'; 10 | } 11 | return adapter.createPublisher(config); 12 | }; 13 | 14 | ParseMessageQueue.createSubscriber = function (config: any): void { 15 | const adapter = loadAdapter(config.messageQueueAdapter, EventEmitterMQ, config); 16 | if (typeof adapter.createSubscriber !== 'function') { 17 | throw 'messageQueueAdapter should have createSubscriber()'; 18 | } 19 | return adapter.createSubscriber(config); 20 | }; 21 | 22 | export { ParseMessageQueue }; 23 | -------------------------------------------------------------------------------- /src/Push/PushQueue.js: -------------------------------------------------------------------------------- 1 | import { ParseMessageQueue } from '../ParseMessageQueue'; 2 | import rest from '../rest'; 3 | import { applyDeviceTokenExists } from './utils'; 4 | import Parse from 'parse/node'; 5 | 6 | const PUSH_CHANNEL = 'parse-server-push'; 7 | const DEFAULT_BATCH_SIZE = 100; 8 | 9 | export class PushQueue { 10 | parsePublisher: Object; 11 | channel: String; 12 | batchSize: Number; 13 | 14 | // config object of the publisher, right now it only contains the redisURL, 15 | // but we may extend it later. 16 | constructor(config: any = {}) { 17 | this.channel = config.channel || PushQueue.defaultPushChannel(); 18 | this.batchSize = config.batchSize || DEFAULT_BATCH_SIZE; 19 | this.parsePublisher = ParseMessageQueue.createPublisher(config); 20 | } 21 | 22 | static defaultPushChannel() { 23 | return `${Parse.applicationId}-${PUSH_CHANNEL}`; 24 | } 25 | 26 | enqueue(body, where, config, auth, pushStatus) { 27 | const limit = this.batchSize; 28 | 29 | where = applyDeviceTokenExists(where); 30 | 31 | // Order by objectId so no impact on the DB 32 | const order = 'objectId'; 33 | return Promise.resolve() 34 | .then(() => { 35 | return rest.find(config, auth, '_Installation', where, { 36 | limit: 0, 37 | count: true, 38 | }); 39 | }) 40 | .then(({ results, count }) => { 41 | if (!results || count == 0) { 42 | return pushStatus.complete(); 43 | } 44 | pushStatus.setRunning(Math.ceil(count / limit)); 45 | let skip = 0; 46 | while (skip < count) { 47 | const query = { 48 | where, 49 | limit, 50 | skip, 51 | order, 52 | }; 53 | 54 | const pushWorkItem = { 55 | body, 56 | query, 57 | pushStatus: { objectId: pushStatus.objectId }, 58 | applicationId: config.applicationId, 59 | }; 60 | this.parsePublisher.publish(this.channel, JSON.stringify(pushWorkItem)); 61 | skip += limit; 62 | } 63 | }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Routers/AnalyticsRouter.js: -------------------------------------------------------------------------------- 1 | // AnalyticsRouter.js 2 | import PromiseRouter from '../PromiseRouter'; 3 | 4 | function appOpened(req) { 5 | const analyticsController = req.config.analyticsController; 6 | return analyticsController.appOpened(req); 7 | } 8 | 9 | function trackEvent(req) { 10 | const analyticsController = req.config.analyticsController; 11 | return analyticsController.trackEvent(req); 12 | } 13 | 14 | export class AnalyticsRouter extends PromiseRouter { 15 | mountRoutes() { 16 | this.route('POST', '/events/AppOpened', appOpened); 17 | this.route('POST', '/events/:eventName', trackEvent); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Routers/AudiencesRouter.js: -------------------------------------------------------------------------------- 1 | import ClassesRouter from './ClassesRouter'; 2 | import rest from '../rest'; 3 | import * as middleware from '../middlewares'; 4 | 5 | export class AudiencesRouter extends ClassesRouter { 6 | className() { 7 | return '_Audience'; 8 | } 9 | 10 | handleFind(req) { 11 | const body = Object.assign(req.body || {}, ClassesRouter.JSONFromQuery(req.query)); 12 | const options = ClassesRouter.optionsFromBody(body, req.config.defaultLimit); 13 | 14 | return rest 15 | .find( 16 | req.config, 17 | req.auth, 18 | '_Audience', 19 | body.where, 20 | options, 21 | req.info.clientSDK, 22 | req.info.context 23 | ) 24 | .then(response => { 25 | response.results.forEach(item => { 26 | item.query = JSON.parse(item.query); 27 | }); 28 | 29 | return { response: response }; 30 | }); 31 | } 32 | 33 | handleGet(req) { 34 | return super.handleGet(req).then(data => { 35 | data.response.query = JSON.parse(data.response.query); 36 | 37 | return data; 38 | }); 39 | } 40 | 41 | mountRoutes() { 42 | this.route('GET', '/push_audiences', middleware.promiseEnforceMasterKeyAccess, req => { 43 | return this.handleFind(req); 44 | }); 45 | this.route( 46 | 'GET', 47 | '/push_audiences/:objectId', 48 | middleware.promiseEnforceMasterKeyAccess, 49 | req => { 50 | return this.handleGet(req); 51 | } 52 | ); 53 | this.route('POST', '/push_audiences', middleware.promiseEnforceMasterKeyAccess, req => { 54 | return this.handleCreate(req); 55 | }); 56 | this.route( 57 | 'PUT', 58 | '/push_audiences/:objectId', 59 | middleware.promiseEnforceMasterKeyAccess, 60 | req => { 61 | return this.handleUpdate(req); 62 | } 63 | ); 64 | this.route( 65 | 'DELETE', 66 | '/push_audiences/:objectId', 67 | middleware.promiseEnforceMasterKeyAccess, 68 | req => { 69 | return this.handleDelete(req); 70 | } 71 | ); 72 | } 73 | } 74 | 75 | export default AudiencesRouter; 76 | -------------------------------------------------------------------------------- /src/Routers/FeaturesRouter.js: -------------------------------------------------------------------------------- 1 | import { version } from '../../package.json'; 2 | import PromiseRouter from '../PromiseRouter'; 3 | import * as middleware from '../middlewares'; 4 | 5 | export class FeaturesRouter extends PromiseRouter { 6 | mountRoutes() { 7 | this.route('GET', '/serverInfo', middleware.promiseEnforceMasterKeyAccess, req => { 8 | const { config } = req; 9 | const features = { 10 | globalConfig: { 11 | create: true, 12 | read: true, 13 | update: true, 14 | delete: true, 15 | }, 16 | hooks: { 17 | create: true, 18 | read: true, 19 | update: true, 20 | delete: true, 21 | }, 22 | cloudCode: { 23 | jobs: true, 24 | }, 25 | logs: { 26 | level: true, 27 | size: true, 28 | order: true, 29 | until: true, 30 | from: true, 31 | }, 32 | push: { 33 | immediatePush: config.hasPushSupport, 34 | scheduledPush: config.hasPushScheduledSupport, 35 | storedPushData: config.hasPushSupport, 36 | pushAudiences: true, 37 | localization: true, 38 | }, 39 | schemas: { 40 | addField: true, 41 | removeField: true, 42 | addClass: true, 43 | removeClass: true, 44 | clearAllDataFromClass: true, 45 | exportClass: false, 46 | editClassLevelPermissions: true, 47 | editPointerPermissions: true, 48 | }, 49 | settings: { 50 | securityCheck: !!config.security?.enableCheck, 51 | }, 52 | }; 53 | 54 | return { 55 | response: { 56 | features: features, 57 | parseServerVersion: version, 58 | }, 59 | }; 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Routers/GraphQLRouter.js: -------------------------------------------------------------------------------- 1 | import Parse from 'parse/node'; 2 | import PromiseRouter from '../PromiseRouter'; 3 | import * as middleware from '../middlewares'; 4 | 5 | const GraphQLConfigPath = '/graphql-config'; 6 | 7 | export class GraphQLRouter extends PromiseRouter { 8 | async getGraphQLConfig(req) { 9 | const result = await req.config.parseGraphQLController.getGraphQLConfig(); 10 | return { 11 | response: result, 12 | }; 13 | } 14 | 15 | async updateGraphQLConfig(req) { 16 | if (req.auth.isReadOnly) { 17 | throw new Parse.Error( 18 | Parse.Error.OPERATION_FORBIDDEN, 19 | "read-only masterKey isn't allowed to update the GraphQL config." 20 | ); 21 | } 22 | const data = await req.config.parseGraphQLController.updateGraphQLConfig(req.body?.params || {}); 23 | return { 24 | response: data, 25 | }; 26 | } 27 | 28 | mountRoutes() { 29 | this.route('GET', GraphQLConfigPath, middleware.promiseEnforceMasterKeyAccess, req => { 30 | return this.getGraphQLConfig(req); 31 | }); 32 | this.route('PUT', GraphQLConfigPath, middleware.promiseEnforceMasterKeyAccess, req => { 33 | return this.updateGraphQLConfig(req); 34 | }); 35 | } 36 | } 37 | 38 | export default GraphQLRouter; 39 | -------------------------------------------------------------------------------- /src/Routers/InstallationsRouter.js: -------------------------------------------------------------------------------- 1 | // InstallationsRouter.js 2 | 3 | import ClassesRouter from './ClassesRouter'; 4 | import rest from '../rest'; 5 | import { promiseEnsureIdempotency } from '../middlewares'; 6 | 7 | export class InstallationsRouter extends ClassesRouter { 8 | className() { 9 | return '_Installation'; 10 | } 11 | 12 | handleFind(req) { 13 | const body = Object.assign(req.body || {}, ClassesRouter.JSONFromQuery(req.query)); 14 | const options = ClassesRouter.optionsFromBody(body, req.config.defaultLimit); 15 | return rest 16 | .find( 17 | req.config, 18 | req.auth, 19 | '_Installation', 20 | body.where, 21 | options, 22 | req.info.clientSDK, 23 | req.info.context 24 | ) 25 | .then(response => { 26 | return { response: response }; 27 | }); 28 | } 29 | 30 | mountRoutes() { 31 | this.route('GET', '/installations', req => { 32 | return this.handleFind(req); 33 | }); 34 | this.route('GET', '/installations/:objectId', req => { 35 | return this.handleGet(req); 36 | }); 37 | this.route('POST', '/installations', promiseEnsureIdempotency, req => { 38 | return this.handleCreate(req); 39 | }); 40 | this.route('PUT', '/installations/:objectId', promiseEnsureIdempotency, req => { 41 | return this.handleUpdate(req); 42 | }); 43 | this.route('DELETE', '/installations/:objectId', req => { 44 | return this.handleDelete(req); 45 | }); 46 | } 47 | } 48 | 49 | export default InstallationsRouter; 50 | -------------------------------------------------------------------------------- /src/Routers/LogsRouter.js: -------------------------------------------------------------------------------- 1 | import { Parse } from 'parse/node'; 2 | import PromiseRouter from '../PromiseRouter'; 3 | import * as middleware from '../middlewares'; 4 | 5 | export class LogsRouter extends PromiseRouter { 6 | mountRoutes() { 7 | this.route( 8 | 'GET', 9 | '/scriptlog', 10 | middleware.promiseEnforceMasterKeyAccess, 11 | this.validateRequest, 12 | req => { 13 | return this.handleGET(req); 14 | } 15 | ); 16 | } 17 | 18 | validateRequest(req) { 19 | if (!req.config || !req.config.loggerController) { 20 | throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'Logger adapter is not available'); 21 | } 22 | } 23 | 24 | // Returns a promise for a {response} object. 25 | // query params: 26 | // level (optional) Level of logging you want to query for (info || error) 27 | // from (optional) Start time for the search. Defaults to 1 week ago. 28 | // until (optional) End time for the search. Defaults to current time. 29 | // order (optional) Direction of results returned, either “asc” or “desc”. Defaults to “desc”. 30 | // size (optional) Number of rows returned by search. Defaults to 10 31 | // n same as size, overrides size if set 32 | handleGET(req) { 33 | const from = req.query.from; 34 | const until = req.query.until; 35 | let size = req.query.size; 36 | if (req.query.n) { 37 | size = req.query.n; 38 | } 39 | 40 | const order = req.query.order; 41 | const level = req.query.level; 42 | const options = { 43 | from, 44 | until, 45 | size, 46 | order, 47 | level, 48 | }; 49 | 50 | return req.config.loggerController.getLogs(options).then(result => { 51 | return Promise.resolve({ 52 | response: result, 53 | }); 54 | }); 55 | } 56 | } 57 | 58 | export default LogsRouter; 59 | -------------------------------------------------------------------------------- /src/Routers/PurgeRouter.js: -------------------------------------------------------------------------------- 1 | import PromiseRouter from '../PromiseRouter'; 2 | import * as middleware from '../middlewares'; 3 | import Parse from 'parse/node'; 4 | 5 | export class PurgeRouter extends PromiseRouter { 6 | handlePurge(req) { 7 | if (req.auth.isReadOnly) { 8 | throw new Parse.Error( 9 | Parse.Error.OPERATION_FORBIDDEN, 10 | "read-only masterKey isn't allowed to purge a schema." 11 | ); 12 | } 13 | return req.config.database 14 | .purgeCollection(req.params.className) 15 | .then(() => { 16 | var cacheAdapter = req.config.cacheController; 17 | if (req.params.className == '_Session') { 18 | cacheAdapter.user.clear(); 19 | } else if (req.params.className == '_Role') { 20 | cacheAdapter.role.clear(); 21 | } 22 | return { response: {} }; 23 | }) 24 | .catch(error => { 25 | if (!error || (error && error.code === Parse.Error.OBJECT_NOT_FOUND)) { 26 | return { response: {} }; 27 | } 28 | throw error; 29 | }); 30 | } 31 | 32 | mountRoutes() { 33 | this.route('DELETE', '/purge/:className', middleware.promiseEnforceMasterKeyAccess, req => { 34 | return this.handlePurge(req); 35 | }); 36 | } 37 | } 38 | 39 | export default PurgeRouter; 40 | -------------------------------------------------------------------------------- /src/Routers/RolesRouter.js: -------------------------------------------------------------------------------- 1 | import ClassesRouter from './ClassesRouter'; 2 | 3 | export class RolesRouter extends ClassesRouter { 4 | className() { 5 | return '_Role'; 6 | } 7 | 8 | mountRoutes() { 9 | this.route('GET', '/roles', req => { 10 | return this.handleFind(req); 11 | }); 12 | this.route('GET', '/roles/:objectId', req => { 13 | return this.handleGet(req); 14 | }); 15 | this.route('POST', '/roles', req => { 16 | return this.handleCreate(req); 17 | }); 18 | this.route('PUT', '/roles/:objectId', req => { 19 | return this.handleUpdate(req); 20 | }); 21 | this.route('DELETE', '/roles/:objectId', req => { 22 | return this.handleDelete(req); 23 | }); 24 | } 25 | } 26 | 27 | export default RolesRouter; 28 | -------------------------------------------------------------------------------- /src/Routers/SecurityRouter.js: -------------------------------------------------------------------------------- 1 | import PromiseRouter from '../PromiseRouter'; 2 | import * as middleware from '../middlewares'; 3 | import CheckRunner from '../Security/CheckRunner'; 4 | 5 | export class SecurityRouter extends PromiseRouter { 6 | mountRoutes() { 7 | this.route( 8 | 'GET', 9 | '/security', 10 | middleware.promiseEnforceMasterKeyAccess, 11 | this._enforceSecurityCheckEnabled, 12 | async req => { 13 | const report = await new CheckRunner(req.config.security).run(); 14 | return { 15 | status: 200, 16 | response: report, 17 | }; 18 | } 19 | ); 20 | } 21 | 22 | async _enforceSecurityCheckEnabled(req) { 23 | const config = req.config; 24 | if (!config.security || !config.security.enableCheck) { 25 | const error = new Error(); 26 | error.status = 409; 27 | error.message = 'Enable Parse Server option `security.enableCheck` to run security check.'; 28 | throw error; 29 | } 30 | } 31 | } 32 | 33 | export default SecurityRouter; 34 | -------------------------------------------------------------------------------- /src/Security/CheckGroup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A group of security checks. 3 | * @interface 4 | * @memberof module:SecurityCheck 5 | */ 6 | class CheckGroup { 7 | constructor() { 8 | this._name = this.setName(); 9 | this._checks = this.setChecks(); 10 | } 11 | 12 | /** 13 | * The security check group name; to be overridden by child class. 14 | */ 15 | setName() { 16 | throw `Check group has no name.`; 17 | } 18 | name() { 19 | return this._name; 20 | } 21 | 22 | /** 23 | * The security checks; to be overridden by child class. 24 | */ 25 | setChecks() { 26 | throw `Check group has no checks.`; 27 | } 28 | checks() { 29 | return this._checks; 30 | } 31 | 32 | /** 33 | * Runs all checks. 34 | */ 35 | async run() { 36 | for (const check of this._checks) { 37 | check.run(); 38 | } 39 | } 40 | } 41 | 42 | module.exports = CheckGroup; 43 | -------------------------------------------------------------------------------- /src/Security/CheckGroups/CheckGroupDatabase.js: -------------------------------------------------------------------------------- 1 | import { Check } from '../Check'; 2 | import CheckGroup from '../CheckGroup'; 3 | import Config from '../../Config'; 4 | import Parse from 'parse/node'; 5 | 6 | /** 7 | * The security checks group for Parse Server configuration. 8 | * Checks common Parse Server parameters such as access keys 9 | * @memberof module:SecurityCheck 10 | */ 11 | class CheckGroupDatabase extends CheckGroup { 12 | setName() { 13 | return 'Database'; 14 | } 15 | setChecks() { 16 | const config = Config.get(Parse.applicationId); 17 | const databaseAdapter = config.database.adapter; 18 | const databaseUrl = databaseAdapter._uri; 19 | return [ 20 | new Check({ 21 | title: 'Secure database password', 22 | warning: 'The database password is insecure and vulnerable to brute force attacks.', 23 | solution: 24 | 'Choose a longer and/or more complex password with a combination of upper- and lowercase characters, numbers and special characters.', 25 | check: () => { 26 | const password = databaseUrl.match(/\/\/\S+:(\S+)@/)[1]; 27 | const hasUpperCase = /[A-Z]/.test(password); 28 | const hasLowerCase = /[a-z]/.test(password); 29 | const hasNumbers = /\d/.test(password); 30 | const hasNonAlphasNumerics = /\W/.test(password); 31 | // Ensure length 32 | if (password.length < 14) { 33 | throw 1; 34 | } 35 | // Ensure at least 3 out of 4 requirements passed 36 | if (hasUpperCase + hasLowerCase + hasNumbers + hasNonAlphasNumerics < 3) { 37 | throw 1; 38 | } 39 | }, 40 | }), 41 | ]; 42 | } 43 | } 44 | 45 | module.exports = CheckGroupDatabase; 46 | -------------------------------------------------------------------------------- /src/Security/CheckGroups/CheckGroups.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @memberof module:SecurityCheck 3 | */ 4 | 5 | /** 6 | * The list of security check groups. 7 | */ 8 | export { default as CheckGroupDatabase } from './CheckGroupDatabase'; 9 | export { default as CheckGroupServerConfig } from './CheckGroupServerConfig'; 10 | -------------------------------------------------------------------------------- /src/SharedRest.js: -------------------------------------------------------------------------------- 1 | const classesWithMasterOnlyAccess = [ 2 | '_JobStatus', 3 | '_PushStatus', 4 | '_Hooks', 5 | '_GlobalConfig', 6 | '_JobSchedule', 7 | '_Idempotency', 8 | ]; 9 | // Disallowing access to the _Role collection except by master key 10 | function enforceRoleSecurity(method, className, auth) { 11 | if (className === '_Installation' && !auth.isMaster && !auth.isMaintenance) { 12 | if (method === 'delete' || method === 'find') { 13 | const error = `Clients aren't allowed to perform the ${method} operation on the installation collection.`; 14 | throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); 15 | } 16 | } 17 | 18 | //all volatileClasses are masterKey only 19 | if ( 20 | classesWithMasterOnlyAccess.indexOf(className) >= 0 && 21 | !auth.isMaster && 22 | !auth.isMaintenance 23 | ) { 24 | const error = `Clients aren't allowed to perform the ${method} operation on the ${className} collection.`; 25 | throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); 26 | } 27 | 28 | // readOnly masterKey is not allowed 29 | if (auth.isReadOnly && (method === 'delete' || method === 'create' || method === 'update')) { 30 | const error = `read-only masterKey isn't allowed to perform the ${method} operation.`; 31 | throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); 32 | } 33 | } 34 | 35 | module.exports = { 36 | enforceRoleSecurity, 37 | }; 38 | -------------------------------------------------------------------------------- /src/cache.js: -------------------------------------------------------------------------------- 1 | import { InMemoryCache } from './Adapters/Cache/InMemoryCache'; 2 | 3 | export var AppCache = new InMemoryCache({ ttl: NaN }); 4 | export default AppCache; 5 | -------------------------------------------------------------------------------- /src/cli/definitions/parse-live-query-server.js: -------------------------------------------------------------------------------- 1 | const LiveQueryServerOptions = require('../../Options/Definitions').LiveQueryServerOptions; 2 | export default LiveQueryServerOptions; 3 | -------------------------------------------------------------------------------- /src/cli/definitions/parse-server.js: -------------------------------------------------------------------------------- 1 | const ParseServerDefinitions = require('../../Options/Definitions').ParseServerOptions; 2 | export default ParseServerDefinitions; 3 | -------------------------------------------------------------------------------- /src/cli/parse-live-query-server.js: -------------------------------------------------------------------------------- 1 | import definitions from './definitions/parse-live-query-server'; 2 | import runner from './utils/runner'; 3 | import { ParseServer } from '../index'; 4 | 5 | runner({ 6 | definitions, 7 | start: function (program, options, logOptions) { 8 | logOptions(); 9 | ParseServer.createLiveQueryServer(undefined, options); 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/cli/utils/runner.js: -------------------------------------------------------------------------------- 1 | import program from './commander'; 2 | 3 | function logStartupOptions(options) { 4 | if (!options.verbose) { 5 | return; 6 | } 7 | // Keys that may include sensitive information that will be redacted in logs 8 | const keysToRedact = [ 9 | 'databaseAdapter', 10 | 'databaseURI', 11 | 'masterKey', 12 | 'maintenanceKey', 13 | 'push', 14 | ]; 15 | for (const key in options) { 16 | let value = options[key]; 17 | if (keysToRedact.includes(key)) { 18 | value = ''; 19 | } 20 | if (typeof value === 'object') { 21 | try { 22 | value = JSON.stringify(value); 23 | } catch (e) { 24 | if (value && value.constructor && value.constructor.name) { 25 | value = value.constructor.name; 26 | } 27 | } 28 | } 29 | /* eslint-disable no-console */ 30 | console.log(`${key}: ${value}`); 31 | /* eslint-enable no-console */ 32 | } 33 | } 34 | 35 | export default function ({ definitions, help, usage, start }) { 36 | program.loadDefinitions(definitions); 37 | if (usage) { 38 | program.usage(usage); 39 | } 40 | if (help) { 41 | program.on('--help', help); 42 | } 43 | program.parse(process.argv, process.env); 44 | 45 | const options = program.getOptions(); 46 | start(program, options, function () { 47 | logStartupOptions(options); 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /src/cloud-code/Parse.Server.js: -------------------------------------------------------------------------------- 1 | const ParseServer = {}; 2 | /** 3 | * ... 4 | * 5 | * @memberof Parse.Server 6 | * @property {String} global Rate limit based on the number of requests made by all users. 7 | * @property {String} session Rate limit based on the sessionToken. 8 | * @property {String} user Rate limit based on the user ID. 9 | * @property {String} ip Rate limit based on the request ip. 10 | * ... 11 | */ 12 | ParseServer.RateLimitZone = Object.freeze({ 13 | global: 'global', 14 | session: 'session', 15 | user: 'user', 16 | ip: 'ip', 17 | }); 18 | 19 | module.exports = ParseServer; 20 | -------------------------------------------------------------------------------- /src/cryptoUtils.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { randomBytes, createHash } from 'crypto'; 4 | 5 | // Returns a new random hex string of the given even size. 6 | export function randomHexString(size: number): string { 7 | if (size === 0) { 8 | throw new Error('Zero-length randomHexString is useless.'); 9 | } 10 | if (size % 2 !== 0) { 11 | throw new Error('randomHexString size must be divisible by 2.'); 12 | } 13 | return randomBytes(size / 2).toString('hex'); 14 | } 15 | 16 | // Returns a new random alphanumeric string of the given size. 17 | // 18 | // Note: to simplify implementation, the result has slight modulo bias, 19 | // because chars length of 62 doesn't divide the number of all bytes 20 | // (256) evenly. Such bias is acceptable for most cases when the output 21 | // length is long enough and doesn't need to be uniform. 22 | export function randomString(size: number): string { 23 | if (size === 0) { 24 | throw new Error('Zero-length randomString is useless.'); 25 | } 26 | const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + 'abcdefghijklmnopqrstuvwxyz' + '0123456789'; 27 | let objectId = ''; 28 | const bytes = randomBytes(size); 29 | for (let i = 0; i < bytes.length; ++i) { 30 | objectId += chars[bytes.readUInt8(i) % chars.length]; 31 | } 32 | return objectId; 33 | } 34 | 35 | // Returns a new random alphanumeric string suitable for object ID. 36 | export function newObjectId(size: number = 10): string { 37 | return randomString(size); 38 | } 39 | 40 | // Returns a new random hex string suitable for secure tokens. 41 | export function newToken(): string { 42 | return randomHexString(32); 43 | } 44 | 45 | export function md5Hash(string: string): string { 46 | return createHash('md5').update(string).digest('hex'); 47 | } 48 | -------------------------------------------------------------------------------- /src/defaults.js: -------------------------------------------------------------------------------- 1 | import { nullParser } from './Options/parsers'; 2 | const { ParseServerOptions } = require('./Options/Definitions'); 3 | const logsFolder = (() => { 4 | let folder = './logs/'; 5 | if (typeof process !== 'undefined' && process.env.TESTING === '1') { 6 | folder = './test_logs/'; 7 | } 8 | if (process.env.PARSE_SERVER_LOGS_FOLDER) { 9 | folder = nullParser(process.env.PARSE_SERVER_LOGS_FOLDER); 10 | } 11 | return folder; 12 | })(); 13 | 14 | const { verbose, level } = (() => { 15 | const verbose = process.env.VERBOSE ? true : false; 16 | return { verbose, level: verbose ? 'verbose' : undefined }; 17 | })(); 18 | 19 | const DefinitionDefaults = Object.keys(ParseServerOptions).reduce((memo, key) => { 20 | const def = ParseServerOptions[key]; 21 | if (Object.prototype.hasOwnProperty.call(def, 'default')) { 22 | memo[key] = def.default; 23 | } 24 | return memo; 25 | }, {}); 26 | 27 | const computedDefaults = { 28 | jsonLogs: process.env.JSON_LOGS || false, 29 | logsFolder, 30 | verbose, 31 | level, 32 | }; 33 | 34 | export default Object.assign({}, DefinitionDefaults, computedDefaults); 35 | export const DefaultMongoURI = DefinitionDefaults.databaseURI; 36 | -------------------------------------------------------------------------------- /src/deprecated.js: -------------------------------------------------------------------------------- 1 | export function useExternal(name, moduleName) { 2 | return function () { 3 | throw `${name} is not provided by parse-server anymore; please install ${moduleName}`; 4 | }; 5 | } 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import ParseServer from './ParseServer'; 2 | import FileSystemAdapter from '@parse/fs-files-adapter'; 3 | import InMemoryCacheAdapter from './Adapters/Cache/InMemoryCacheAdapter'; 4 | import NullCacheAdapter from './Adapters/Cache/NullCacheAdapter'; 5 | import RedisCacheAdapter from './Adapters/Cache/RedisCacheAdapter'; 6 | import LRUCacheAdapter from './Adapters/Cache/LRUCache.js'; 7 | import * as TestUtils from './TestUtils'; 8 | import * as SchemaMigrations from './SchemaMigrations/Migrations'; 9 | import AuthAdapter from './Adapters/Auth/AuthAdapter'; 10 | import { useExternal } from './deprecated'; 11 | import { getLogger } from './logger'; 12 | import { PushWorker } from './Push/PushWorker'; 13 | import { ParseServerOptions } from './Options'; 14 | import { ParseGraphQLServer } from './GraphQL/ParseGraphQLServer'; 15 | 16 | // Factory function 17 | const _ParseServer = function (options: ParseServerOptions) { 18 | const server = new ParseServer(options); 19 | return server; 20 | }; 21 | // Mount the create liveQueryServer 22 | _ParseServer.createLiveQueryServer = ParseServer.createLiveQueryServer; 23 | _ParseServer.startApp = ParseServer.startApp; 24 | 25 | const S3Adapter = useExternal('S3Adapter', '@parse/s3-files-adapter'); 26 | const GCSAdapter = useExternal('GCSAdapter', '@parse/gcs-files-adapter'); 27 | 28 | Object.defineProperty(module.exports, 'logger', { 29 | get: getLogger, 30 | }); 31 | 32 | export default ParseServer; 33 | export { 34 | S3Adapter, 35 | GCSAdapter, 36 | FileSystemAdapter, 37 | InMemoryCacheAdapter, 38 | NullCacheAdapter, 39 | RedisCacheAdapter, 40 | LRUCacheAdapter, 41 | TestUtils, 42 | PushWorker, 43 | ParseGraphQLServer, 44 | _ParseServer as ParseServer, 45 | SchemaMigrations, 46 | AuthAdapter, 47 | }; 48 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import defaults from './defaults'; 3 | import { WinstonLoggerAdapter } from './Adapters/Logger/WinstonLoggerAdapter'; 4 | import { LoggerController } from './Controllers/LoggerController'; 5 | 6 | // Used for Separate Live Query Server 7 | function defaultLogger() { 8 | const options = { 9 | logsFolder: defaults.logsFolder, 10 | jsonLogs: defaults.jsonLogs, 11 | verbose: defaults.verbose, 12 | silent: defaults.silent, 13 | }; 14 | const adapter = new WinstonLoggerAdapter(options); 15 | return new LoggerController(adapter, null, options); 16 | } 17 | 18 | let logger = defaultLogger(); 19 | 20 | export function setLogger(aLogger) { 21 | logger = aLogger; 22 | } 23 | 24 | export function getLogger() { 25 | return logger; 26 | } 27 | 28 | // for: `import logger from './logger'` 29 | Object.defineProperty(module.exports, 'default', { 30 | get: getLogger, 31 | }); 32 | 33 | // for: `import { logger } from './logger'` 34 | Object.defineProperty(module.exports, 'logger', { 35 | get: getLogger, 36 | }); 37 | -------------------------------------------------------------------------------- /src/password.js: -------------------------------------------------------------------------------- 1 | // Tools for encrypting and decrypting passwords. 2 | // Basically promise-friendly wrappers for bcrypt. 3 | var bcrypt = require('bcryptjs'); 4 | 5 | try { 6 | const _bcrypt = require('@node-rs/bcrypt'); 7 | bcrypt = { 8 | hash: _bcrypt.hash, 9 | compare: _bcrypt.verify, 10 | }; 11 | } catch (e) { 12 | /* */ 13 | } 14 | 15 | // Returns a promise for a hashed password string. 16 | function hash(password) { 17 | return bcrypt.hash(password, 10); 18 | } 19 | 20 | // Returns a promise for whether this password compares to equal this 21 | // hashed password. 22 | function compare(password, hashedPassword) { 23 | // Cannot bcrypt compare when one is undefined 24 | if (!password || !hashedPassword) { 25 | return Promise.resolve(false); 26 | } 27 | return bcrypt.compare(password, hashedPassword); 28 | } 29 | 30 | module.exports = { 31 | hash: hash, 32 | compare: compare, 33 | }; 34 | -------------------------------------------------------------------------------- /src/requiredParameter.js: -------------------------------------------------------------------------------- 1 | /** @flow */ 2 | export default (errorMessage: string): any => { 3 | throw errorMessage; 4 | }; 5 | -------------------------------------------------------------------------------- /src/vendor/README.md: -------------------------------------------------------------------------------- 1 | # mongoUrl 2 | 3 | A fork of node's `url` module, with the modification that commas and colons are 4 | allowed in hostnames. While this results in a slightly incorrect parsed result, 5 | as the hostname field for a mongodb should be an array of replica sets, it's 6 | good enough to let us pull out and escape the auth portion of the URL. 7 | 8 | https://github.com/parse-community/parse-server/pull/986 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2015", 5 | "declaration": true, 6 | "emitDeclarationOnly": true, 7 | "outDir": "types", 8 | "noImplicitAny": false, 9 | "allowJs": false, 10 | "skipLibCheck": true, 11 | "paths": { 12 | "deepcopy": ["./types/@types/deepcopy"], 13 | } 14 | }, 15 | "include": [ 16 | "src/*.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /types/@types/@parse/fs-files-adapter/index.d.ts: -------------------------------------------------------------------------------- 1 | // TODO: Remove when @parse/fs-files-adapter is typed 2 | declare module '@parse/fs-files-adapter' { 3 | const FileSystemAdapter: any; 4 | export default FileSystemAdapter; 5 | } 6 | -------------------------------------------------------------------------------- /types/@types/deepcopy/index.d.ts: -------------------------------------------------------------------------------- 1 | // TODO: Remove when https://github.com/sasaplus1/deepcopy.js/issues/278 is fixed 2 | declare type Customizer = (value: any, valueType: string) => unknown; 3 | declare type Options = Customizer | { customizer: Customizer }; 4 | declare function deepcopy(value: T, options?: Options): T; 5 | export default deepcopy; 6 | -------------------------------------------------------------------------------- /types/LiveQuery/ParseLiveQueryServer.d.ts: -------------------------------------------------------------------------------- 1 | import { Auth } from '../Auth'; 2 | declare class ParseLiveQueryServer { 3 | server: any; 4 | config: any; 5 | clients: Map; 6 | subscriptions: Map; 7 | parseWebSocketServer: any; 8 | keyPairs: any; 9 | subscriber: any; 10 | authCache: any; 11 | cacheController: any; 12 | constructor(server: any, config?: any, parseServerConfig?: any); 13 | connect(): Promise; 14 | shutdown(): Promise; 15 | _createSubscribers(): void; 16 | _inflateParseObject(message: any): void; 17 | _onAfterDelete(message: any): Promise; 18 | _onAfterSave(message: any): Promise; 19 | _onConnect(parseWebsocket: any): void; 20 | _matchesSubscription(parseObject: any, subscription: any): boolean; 21 | _clearCachedRoles(userId: string): Promise; 22 | getAuthForSessionToken(sessionToken?: string): Promise<{ 23 | auth?: Auth; 24 | userId?: string; 25 | }>; 26 | _matchesCLP(classLevelPermissions?: any, object?: any, client?: any, requestId?: number, op?: string): Promise; 27 | _filterSensitiveData(classLevelPermissions?: any, res?: any, client?: any, requestId?: number, op?: string, query?: any): Promise; 28 | _getCLPOperation(query: any): "get" | "find"; 29 | _verifyACL(acl: any, token: string): Promise; 30 | getAuthFromClient(client: any, requestId: number, sessionToken?: string): Promise; 31 | _checkWatchFields(client: any, requestId: any, message: any): any; 32 | _matchesACL(acl: any, client: any, requestId: number): Promise; 33 | _handleConnect(parseWebsocket: any, request: any): Promise; 34 | _hasMasterKey(request: any, validKeyPairs: any): boolean; 35 | _validateKeys(request: any, validKeyPairs: any): boolean; 36 | _handleSubscribe(parseWebsocket: any, request: any): Promise; 37 | _handleUpdateSubscription(parseWebsocket: any, request: any): any; 38 | _handleUnsubscribe(parseWebsocket: any, request: any, notifyClient?: boolean): any; 39 | } 40 | export { ParseLiveQueryServer }; 41 | -------------------------------------------------------------------------------- /types/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js'; 2 | import tseslint from 'typescript-eslint'; 3 | import expectType from 'eslint-plugin-expect-type/configs/recommended'; 4 | 5 | export default tseslint.config({ 6 | files: ['**/*.js', '**/*.ts'], 7 | extends: [ 8 | expectType, 9 | eslint.configs.recommended, 10 | ...tseslint.configs.recommended, 11 | ...tseslint.configs.recommendedTypeChecked, 12 | ], 13 | plugins: { 14 | '@typescript-eslint': tseslint.plugin, 15 | }, 16 | rules: { 17 | '@typescript-eslint/no-unused-vars': 'off', 18 | '@typescript-eslint/no-unused-expressions': 'off', 19 | '@typescript-eslint/no-unsafe-call': 'off', 20 | "@typescript-eslint/no-explicit-any": "off", 21 | "@typescript-eslint/no-unsafe-return": "off", 22 | }, 23 | languageOptions: { 24 | parser: tseslint.parser, 25 | parserOptions: { 26 | projectService: true, 27 | tsconfigRootDir: import.meta.dirname, 28 | }, 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import ParseServer from './ParseServer'; 2 | import FileSystemAdapter from '@parse/fs-files-adapter'; 3 | import InMemoryCacheAdapter from './Adapters/Cache/InMemoryCacheAdapter'; 4 | import NullCacheAdapter from './Adapters/Cache/NullCacheAdapter'; 5 | import RedisCacheAdapter from './Adapters/Cache/RedisCacheAdapter'; 6 | import LRUCacheAdapter from './Adapters/Cache/LRUCache.js'; 7 | import * as TestUtils from './TestUtils'; 8 | import * as SchemaMigrations from './SchemaMigrations/Migrations'; 9 | import AuthAdapter from './Adapters/Auth/AuthAdapter'; 10 | import { PushWorker } from './Push/PushWorker'; 11 | import { ParseServerOptions } from './Options'; 12 | import { ParseGraphQLServer } from './GraphQL/ParseGraphQLServer'; 13 | declare const _ParseServer: { 14 | (options: ParseServerOptions): ParseServer; 15 | createLiveQueryServer: typeof ParseServer.createLiveQueryServer; 16 | startApp: typeof ParseServer.startApp; 17 | }; 18 | declare const S3Adapter: any; 19 | declare const GCSAdapter: any; 20 | export default ParseServer; 21 | export { S3Adapter, GCSAdapter, FileSystemAdapter, InMemoryCacheAdapter, NullCacheAdapter, RedisCacheAdapter, LRUCacheAdapter, TestUtils, PushWorker, ParseGraphQLServer, _ParseServer as ParseServer, SchemaMigrations, AuthAdapter, }; 22 | -------------------------------------------------------------------------------- /types/logger.d.ts: -------------------------------------------------------------------------------- 1 | export declare function setLogger(aLogger: any): void; 2 | export declare function getLogger(): any; 3 | -------------------------------------------------------------------------------- /types/tests.ts: -------------------------------------------------------------------------------- 1 | import ParseServer, { FileSystemAdapter } from 'parse-server'; 2 | 3 | async function server() { 4 | // $ExpectType ParseServer 5 | const parseServer = await ParseServer.startApp({}); 6 | 7 | // $ExpectType void 8 | await parseServer.handleShutdown(); 9 | 10 | // $ExpectType any 11 | parseServer.app; 12 | 13 | // $ExpectType any 14 | ParseServer.app({}); 15 | 16 | // $ExpectType any 17 | ParseServer.promiseRouter({ appId: 'appId' }); 18 | 19 | // $ExpectType ParseLiveQueryServer 20 | await ParseServer.createLiveQueryServer({}, {}, {}); 21 | 22 | // $ExpectType any 23 | ParseServer.verifyServerUrl(); 24 | 25 | // $ExpectError 26 | await ParseServer.startApp(); 27 | 28 | // $ExpectError 29 | ParseServer.promiseRouter(); 30 | 31 | // $ExpectError 32 | await ParseServer.createLiveQueryServer(); 33 | 34 | // $ExpectType ParseServer 35 | const parseServer2 = new ParseServer({}); 36 | 37 | // $ExpectType ParseServer 38 | await parseServer2.start(); 39 | } 40 | 41 | function exports() { 42 | // $ExpectType any 43 | FileSystemAdapter; 44 | } 45 | -------------------------------------------------------------------------------- /types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es6"], 5 | "noImplicitAny": true, 6 | "noImplicitThis": true, 7 | "strictFunctionTypes": true, 8 | "strictNullChecks": true, 9 | "types": [], 10 | "noEmit": true, 11 | "forceConsistentCasingInFileNames": true, 12 | 13 | // If the library is an external module (uses `export`), this allows your test file to import "mylib" instead of "./index". 14 | // If the library is global (cannot be imported via `import` or `require`), leave this out. 15 | "baseUrl": ".", 16 | "paths": { 17 | "parse-server": ["."], 18 | "@parse/fs-files-adapter": ["./@types/@parse/fs-files-adapter"], 19 | } 20 | }, 21 | "include": [ 22 | "tests.ts" 23 | ] 24 | } 25 | --------------------------------------------------------------------------------