├── .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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------