feat: Add TypeScript definitions (#9693)

This commit is contained in:
Diamond Lewis
2025-04-15 06:59:58 -05:00
committed by GitHub
parent 39ef22d5c9
commit e86718fc59
19 changed files with 1482 additions and 36 deletions

View File

@@ -147,6 +147,8 @@ jobs:
- run: npm ci - run: npm ci
- name: Build types - name: Build types
run: npm run build:types run: npm run build:types
- name: Test Types
run: npm run test:types
check-mongo: check-mongo:
strategy: strategy:
matrix: matrix:

2
.npmignore Normal file
View File

@@ -0,0 +1,2 @@
types/tests.ts
types/eslint.config.mjs

View File

@@ -21,6 +21,7 @@
- [Good to Know](#good-to-know) - [Good to Know](#good-to-know)
- [Troubleshooting](#troubleshooting) - [Troubleshooting](#troubleshooting)
- [Please Do's](#please-dos) - [Please Do's](#please-dos)
- [TypeScript Tests](#typescript-tests)
- [Test against Postgres](#test-against-postgres) - [Test against Postgres](#test-against-postgres)
- [Postgres with Docker](#postgres-with-docker) - [Postgres with Docker](#postgres-with-docker)
- [Breaking Changes](#breaking-changes) - [Breaking Changes](#breaking-changes)
@@ -239,6 +240,15 @@ Once you have babel running in watch mode, you can start making changes to parse
* Mocks belong in the `spec/support` folder. * Mocks belong in the `spec/support` folder.
* Please consider if any changes to the [docs](http://docs.parseplatform.org) are needed or add additional sections in the case of an enhancement or feature. * Please consider if any changes to the [docs](http://docs.parseplatform.org) are needed or add additional sections in the case of an enhancement or feature.
#### TypeScript Tests
Type tests are located in [/types/tests.ts](/types/tests.ts) and are responsible for ensuring that the type generation for each class is behaving as expected. Types are generated by manually running the script `npm run build:types`. The generated types are `.d.ts` files located in [/types](/types) and must not be manually changed after generation.
> [!CAUTION]
> An exemption are type changes to `src/Options/index.js` which must be manually updated in `types/Options/index.d.ts`, as these types are not generated via a script.
When developing type definitions you can run `npm run watch:ts` in order to rebuild your changes automatically upon each save. Use `npm run test:types` in order to run types tests against generated `.d.ts` files.
### Test against Postgres ### Test against Postgres
If your pull request introduces a change that may affect the storage or retrieval of objects, you may want to make sure it plays nice with Postgres. If your pull request introduces a change that may affect the storage or retrieval of objects, you may want to make sure it plays nice with Postgres.

View File

@@ -3,7 +3,7 @@ const babelParser = require("@babel/eslint-parser");
const globals = require("globals"); const globals = require("globals");
module.exports = [ module.exports = [
{ {
ignores: ["**/lib/**", "**/coverage/**", "**/out/**"], ignores: ["**/lib/**", "**/coverage/**", "**/out/**", "**/types/**"],
}, },
js.configs.recommended, js.configs.recommended,
{ {

952
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -88,6 +88,7 @@
"cross-env": "7.0.3", "cross-env": "7.0.3",
"deep-diff": "1.0.2", "deep-diff": "1.0.2",
"eslint": "9.23.0", "eslint": "9.23.0",
"eslint-plugin-expect-type": "0.6.2",
"flow-bin": "0.266.1", "flow-bin": "0.266.1",
"form-data": "4.0.2", "form-data": "4.0.2",
"globals": "16.0.0", "globals": "16.0.0",
@@ -109,6 +110,7 @@
"prettier": "2.0.5", "prettier": "2.0.5",
"semantic-release": "24.2.3", "semantic-release": "24.2.3",
"typescript": "5.8.2", "typescript": "5.8.2",
"typescript-eslint": "8.29.0",
"yaml": "2.7.1" "yaml": "2.7.1"
}, },
"scripts": { "scripts": {
@@ -122,6 +124,7 @@
"build": "babel src/ -d lib/ --copy-files --extensions '.ts,.js'", "build": "babel src/ -d lib/ --copy-files --extensions '.ts,.js'",
"build:types": "tsc", "build:types": "tsc",
"watch": "babel --watch src/ -d lib/ --copy-files", "watch": "babel --watch src/ -d lib/ --copy-files",
"watch:ts": "tsc --watch",
"test:mongodb:runnerstart": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=$npm_config_dbversion} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start -t ${MONGODB_TOPOLOGY} --version ${MONGODB_VERSION} -- --port 27017", "test:mongodb:runnerstart": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=$npm_config_dbversion} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start -t ${MONGODB_TOPOLOGY} --version ${MONGODB_VERSION} -- --port 27017",
"test:mongodb:testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=$npm_config_dbversion} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine", "test:mongodb:testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=$npm_config_dbversion} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine",
"test:mongodb": "npm run test:mongodb:runnerstart --dbversion=$npm_config_dbversion && npm run test:mongodb:testonly --dbversion=$npm_config_dbversion", "test:mongodb": "npm run test:mongodb:runnerstart --dbversion=$npm_config_dbversion && npm run test:mongodb:testonly --dbversion=$npm_config_dbversion",
@@ -132,6 +135,7 @@
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=8.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start -t ${MONGODB_TOPOLOGY} --version ${MONGODB_VERSION} -- --port 27017", "pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=8.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start -t ${MONGODB_TOPOLOGY} --version ${MONGODB_VERSION} -- --port 27017",
"testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=8.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine", "testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=8.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine",
"test": "npm run testonly", "test": "npm run testonly",
"test:types": "eslint types/tests.ts -c ./types/eslint.config.mjs",
"posttest": "cross-env mongodb-runner stop --all", "posttest": "cross-env mongodb-runner stop --all",
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=8.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 nyc jasmine", "coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=8.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 nyc jasmine",
"start": "node ./bin/parse-server", "start": "node ./bin/parse-server",

View File

@@ -3,6 +3,7 @@ import Parse from 'parse/node';
import { Subscription } from './Subscription'; import { Subscription } from './Subscription';
import { Client } from './Client'; import { Client } from './Client';
import { ParseWebSocketServer } from './ParseWebSocketServer'; import { ParseWebSocketServer } from './ParseWebSocketServer';
// @ts-ignore
import logger from '../logger'; import logger from '../logger';
import RequestSchema from './RequestSchema'; import RequestSchema from './RequestSchema';
import { matchesQuery, queryHash } from './QueryTools'; import { matchesQuery, queryHash } from './QueryTools';
@@ -26,13 +27,17 @@ import { isDeepStrictEqual } from 'util';
import deepcopy from 'deepcopy'; import deepcopy from 'deepcopy';
class ParseLiveQueryServer { class ParseLiveQueryServer {
clients: Map; server: any;
config: any;
clients: Map<string, any>;
// className -> (queryHash -> subscription) // className -> (queryHash -> subscription)
subscriptions: Object; subscriptions: Map<string, any>;
parseWebSocketServer: Object; parseWebSocketServer: any;
keyPairs: any; keyPairs: any;
// The subscriber we use to get object update from publisher // The subscriber we use to get object update from publisher
subscriber: Object; subscriber: any;
authCache: any;
cacheController: any;
constructor(server: any, config: any = {}, parseServerConfig: any = {}) { constructor(server: any, config: any = {}, parseServerConfig: any = {}) {
this.server = server; this.server = server;
@@ -168,7 +173,7 @@ class ParseLiveQueryServer {
// Message is the JSON object from publisher after inflated. Message.currentParseObject is the ParseObject after changes. // Message is the JSON object from publisher after inflated. Message.currentParseObject is the ParseObject after changes.
// Message.originalParseObject is the original ParseObject. // Message.originalParseObject is the original ParseObject.
async _onAfterDelete(message: any): void { async _onAfterDelete(message: any): Promise<void> {
logger.verbose(Parse.applicationId + 'afterDelete is triggered'); logger.verbose(Parse.applicationId + 'afterDelete is triggered');
let deletedParseObject = message.currentParseObject.toJSON(); let deletedParseObject = message.currentParseObject.toJSON();
@@ -197,7 +202,7 @@ class ParseLiveQueryServer {
const acl = message.currentParseObject.getACL(); const acl = message.currentParseObject.getACL();
// Check CLP // Check CLP
const op = this._getCLPOperation(subscription.query); const op = this._getCLPOperation(subscription.query);
let res = {}; let res: any = {};
try { try {
await this._matchesCLP( await this._matchesCLP(
classLevelPermissions, classLevelPermissions,
@@ -261,7 +266,7 @@ class ParseLiveQueryServer {
// Message is the JSON object from publisher after inflated. Message.currentParseObject is the ParseObject after changes. // Message is the JSON object from publisher after inflated. Message.currentParseObject is the ParseObject after changes.
// Message.originalParseObject is the original ParseObject. // Message.originalParseObject is the original ParseObject.
async _onAfterSave(message: any): void { async _onAfterSave(message: any): Promise<void> {
logger.verbose(Parse.applicationId + 'afterSave is triggered'); logger.verbose(Parse.applicationId + 'afterSave is triggered');
let originalParseObject = null; let originalParseObject = null;
@@ -309,7 +314,7 @@ class ParseLiveQueryServer {
// Set current ParseObject ACL checking promise, if the object does not match // Set current ParseObject ACL checking promise, if the object does not match
// subscription, we do not need to check ACL // subscription, we do not need to check ACL
let currentACLCheckingPromise; let currentACLCheckingPromise;
let res = {}; let res: any = {};
if (!isCurrentSubscriptionMatched) { if (!isCurrentSubscriptionMatched) {
currentACLCheckingPromise = Promise.resolve(false); currentACLCheckingPromise = Promise.resolve(false);
} else { } else {
@@ -548,7 +553,7 @@ class ParseLiveQueryServer {
} }
} }
getAuthForSessionToken(sessionToken: ?string): Promise<{ auth: ?Auth, userId: ?string }> { getAuthForSessionToken(sessionToken?: string): Promise<{ auth?: Auth, userId?: string }> {
if (!sessionToken) { if (!sessionToken) {
return Promise.resolve({}); return Promise.resolve({});
} }
@@ -565,7 +570,7 @@ class ParseLiveQueryServer {
}) })
.catch(error => { .catch(error => {
// There was an error with the session token // There was an error with the session token
const result = {}; const result: any = {};
if (error && error.code === Parse.Error.INVALID_SESSION_TOKEN) { if (error && error.code === Parse.Error.INVALID_SESSION_TOKEN) {
result.error = error; result.error = error;
this.authCache.set(sessionToken, Promise.resolve(result), this.config.cacheTimeout); this.authCache.set(sessionToken, Promise.resolve(result), this.config.cacheTimeout);
@@ -579,12 +584,12 @@ class ParseLiveQueryServer {
} }
async _matchesCLP( async _matchesCLP(
classLevelPermissions: ?any, classLevelPermissions?: any,
object: any, object?: any,
client: any, client?: any,
requestId: number, requestId?: number,
op: string op?: string
): any { ): Promise<any> {
// try to match on user first, less expensive than with roles // try to match on user first, less expensive than with roles
const subscriptionInfo = client.getSubscriptionInfo(requestId); const subscriptionInfo = client.getSubscriptionInfo(requestId);
const aclGroup = ['*']; const aclGroup = ['*'];
@@ -621,12 +626,12 @@ class ParseLiveQueryServer {
} }
async _filterSensitiveData( async _filterSensitiveData(
classLevelPermissions: ?any, classLevelPermissions?: any,
res: any, res?: any,
client: any, client?: any,
requestId: number, requestId?: number,
op: string, op?: string,
query: any query?: any
) { ) {
const subscriptionInfo = client.getSubscriptionInfo(requestId); const subscriptionInfo = client.getSubscriptionInfo(requestId);
const aclGroup = ['*']; const aclGroup = ['*'];
@@ -718,7 +723,7 @@ class ParseLiveQueryServer {
}); });
} }
async getAuthFromClient(client: any, requestId: number, sessionToken: string) { async getAuthFromClient(client: any, requestId: number, sessionToken?: string) {
const getSessionFromClient = () => { const getSessionFromClient = () => {
const subscriptionInfo = client.getSubscriptionInfo(requestId); const subscriptionInfo = client.getSubscriptionInfo(requestId);
if (typeof subscriptionInfo === 'undefined') { if (typeof subscriptionInfo === 'undefined') {
@@ -772,7 +777,7 @@ class ParseLiveQueryServer {
return false; return false;
} }
async _handleConnect(parseWebsocket: any, request: any): any { async _handleConnect(parseWebsocket: any, request: any): Promise<any> {
if (!this._validateKeys(request, this.keyPairs)) { if (!this._validateKeys(request, this.keyPairs)) {
Client.pushError(parseWebsocket, 4, 'Key in request is not valid'); Client.pushError(parseWebsocket, 4, 'Key in request is not valid');
logger.error('Key in request is not valid'); logger.error('Key in request is not valid');
@@ -796,6 +801,7 @@ class ParseLiveQueryServer {
sessionToken: request.sessionToken, sessionToken: request.sessionToken,
useMasterKey: client.hasMasterKey, useMasterKey: client.hasMasterKey,
installationId: request.installationId, installationId: request.installationId,
user: undefined,
}; };
const trigger = getTrigger('@Connect', 'beforeConnect', Parse.applicationId); const trigger = getTrigger('@Connect', 'beforeConnect', Parse.applicationId);
if (trigger) { if (trigger) {
@@ -845,7 +851,7 @@ class ParseLiveQueryServer {
return isValid; return isValid;
} }
async _handleSubscribe(parseWebsocket: any, request: any): any { async _handleSubscribe(parseWebsocket: any, request: any): Promise<any> {
// If we can not find this client, return error to client // If we can not find this client, return error to client
if (!Object.prototype.hasOwnProperty.call(parseWebsocket, 'clientId')) { if (!Object.prototype.hasOwnProperty.call(parseWebsocket, 'clientId')) {
Client.pushError( Client.pushError(
@@ -918,7 +924,7 @@ class ParseLiveQueryServer {
} }
// Add subscriptionInfo to client // Add subscriptionInfo to client
const subscriptionInfo = { const subscriptionInfo: any = {
subscription: subscription, subscription: subscription,
}; };
// Add selected fields, sessionToken and installationId for this subscription if necessary // Add selected fields, sessionToken and installationId for this subscription if necessary

View File

@@ -56,6 +56,11 @@ const connections = new Connections();
// ParseServer works like a constructor of an express app. // ParseServer works like a constructor of an express app.
// https://parseplatform.org/parse-server/api/master/ParseServerOptions.html // https://parseplatform.org/parse-server/api/master/ParseServerOptions.html
class ParseServer { class ParseServer {
_app: any;
config: any;
server: any;
expressApp: any;
liveQueryServer: any;
/** /**
* @constructor * @constructor
* @param {ParseServerOptions} options the parse server initialization options * @param {ParseServerOptions} options the parse server initialization options
@@ -111,7 +116,7 @@ class ParseServer {
const diff = validateKeyNames(options, optionsBlueprint); const diff = validateKeyNames(options, optionsBlueprint);
if (diff.length > 0) { if (diff.length > 0) {
const logger = logging.logger; const logger = (logging as any).logger;
logger.error(`Invalid key(s) found in Parse Server configuration: ${diff.join(', ')}`); logger.error(`Invalid key(s) found in Parse Server configuration: ${diff.join(', ')}`);
} }
@@ -129,7 +134,7 @@ class ParseServer {
Config.validateOptions(options); Config.validateOptions(options);
const allControllers = controllers.getControllers(options); const allControllers = controllers.getControllers(options);
options.state = 'initialized'; (options as any).state = 'initialized';
this.config = Config.put(Object.assign({}, options, allControllers)); this.config = Config.put(Object.assign({}, options, allControllers));
this.config.masterKeyIpsStore = new Map(); this.config.masterKeyIpsStore = new Map();
this.config.maintenanceKeyIpsStore = new Map(); this.config.maintenanceKeyIpsStore = new Map();
@@ -140,7 +145,7 @@ class ParseServer {
* Starts Parse Server as an express app; this promise resolves when Parse Server is ready to accept requests. * Starts Parse Server as an express app; this promise resolves when Parse Server is ready to accept requests.
*/ */
async start() { async start(): Promise<this> {
try { try {
if (this.config.state === 'ok') { if (this.config.state === 'ok') {
return this; return this;
@@ -331,7 +336,7 @@ class ParseServer {
if (!process.env.TESTING) { if (!process.env.TESTING) {
//This causes tests to spew some useless warnings, so disable in test //This causes tests to spew some useless warnings, so disable in test
/* istanbul ignore next */ /* istanbul ignore next */
process.on('uncaughtException', err => { process.on('uncaughtException', (err: any) => {
if (err.code === 'EADDRINUSE') { if (err.code === 'EADDRINUSE') {
// user-friendly message for this common error // user-friendly message for this common error
process.stderr.write(`Unable to listen on port ${err.port}. The port is already in use.`); process.stderr.write(`Unable to listen on port ${err.port}. The port is already in use.`);
@@ -497,7 +502,7 @@ class ParseServer {
httpServer, httpServer,
config: LiveQueryServerOptions, config: LiveQueryServerOptions,
options: ParseServerOptions options: ParseServerOptions
) { ): Promise<ParseLiveQueryServer> {
if (!httpServer || (config && config.port)) { if (!httpServer || (config && config.port)) {
var app = express(); var app = express();
httpServer = require('http').createServer(app); httpServer = require('http').createServer(app);

View File

@@ -1,11 +1,16 @@
{ {
"compilerOptions": { "compilerOptions": {
"module": "commonjs",
"target": "es2015",
"declaration": true, "declaration": true,
"emitDeclarationOnly": true, "emitDeclarationOnly": true,
"outDir": "types", "outDir": "types",
"noImplicitAny": false, "noImplicitAny": false,
"allowJs": false, "allowJs": false,
"skipLibCheck": true "skipLibCheck": true,
"paths": {
"deepcopy": ["./types/@types/deepcopy"],
}
}, },
"include": [ "include": [
"src/*.ts" "src/*.ts"

View File

@@ -0,0 +1,5 @@
// TODO: Remove when @parse/fs-files-adapter is typed
declare module '@parse/fs-files-adapter' {
const FileSystemAdapter: any;
export default FileSystemAdapter;
}

5
types/@types/deepcopy/index.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
// TODO: Remove when https://github.com/sasaplus1/deepcopy.js/issues/278 is fixed
declare type Customizer = (value: any, valueType: string) => unknown;
declare type Options = Customizer | { customizer: Customizer };
declare function deepcopy<T>(value: T, options?: Options): T;
export default deepcopy;

View File

@@ -0,0 +1,40 @@
import { Auth } from '../Auth';
declare class ParseLiveQueryServer {
server: any;
config: any;
clients: Map<string, any>;
subscriptions: Map<string, any>;
parseWebSocketServer: any;
keyPairs: any;
subscriber: any;
authCache: any;
cacheController: any;
constructor(server: any, config?: any, parseServerConfig?: any);
connect(): Promise<void>;
shutdown(): Promise<void>;
_createSubscribers(): void;
_inflateParseObject(message: any): void;
_onAfterDelete(message: any): Promise<void>;
_onAfterSave(message: any): Promise<void>;
_onConnect(parseWebsocket: any): void;
_matchesSubscription(parseObject: any, subscription: any): boolean;
_clearCachedRoles(userId: string): Promise<void>;
getAuthForSessionToken(sessionToken?: string): Promise<{
auth?: Auth;
userId?: string;
}>;
_matchesCLP(classLevelPermissions?: any, object?: any, client?: any, requestId?: number, op?: string): Promise<any>;
_filterSensitiveData(classLevelPermissions?: any, res?: any, client?: any, requestId?: number, op?: string, query?: any): Promise<void>;
_getCLPOperation(query: any): "get" | "find";
_verifyACL(acl: any, token: string): Promise<boolean>;
getAuthFromClient(client: any, requestId: number, sessionToken?: string): Promise<Auth>;
_checkWatchFields(client: any, requestId: any, message: any): any;
_matchesACL(acl: any, client: any, requestId: number): Promise<boolean>;
_handleConnect(parseWebsocket: any, request: any): Promise<any>;
_hasMasterKey(request: any, validKeyPairs: any): boolean;
_validateKeys(request: any, validKeyPairs: any): boolean;
_handleSubscribe(parseWebsocket: any, request: any): Promise<any>;
_handleUpdateSubscription(parseWebsocket: any, request: any): any;
_handleUnsubscribe(parseWebsocket: any, request: any, notifyClient?: boolean): any;
}
export { ParseLiveQueryServer };

251
types/Options/index.d.ts vendored Normal file
View File

@@ -0,0 +1,251 @@
// This file is manually updated to match src/Options/index.js until typed
import { AnalyticsAdapter } from '../Adapters/Analytics/AnalyticsAdapter';
import { CacheAdapter } from '../Adapters/Cache/CacheAdapter';
import { MailAdapter } from '../Adapters/Email/MailAdapter';
import { FilesAdapter } from '../Adapters/Files/FilesAdapter';
import { LoggerAdapter } from '../Adapters/Logger/LoggerAdapter';
import { PubSubAdapter } from '../Adapters/PubSub/PubSubAdapter';
import { StorageAdapter } from '../Adapters/Storage/StorageAdapter';
import { WSSAdapter } from '../Adapters/WebSocketServer/WSSAdapter';
import { CheckGroup } from '../Security/CheckGroup';
export interface SchemaOptions {
definitions: any;
strict?: boolean;
deleteExtraFields?: boolean;
recreateModifiedFields?: boolean;
lockSchemas?: boolean;
beforeMigration?: () => void | Promise<void>;
afterMigration?: () => void | Promise<void>;
}
type Adapter<T> = string | T;
type NumberOrBoolean = number | boolean;
type NumberOrString = number | string;
type ProtectedFields = any;
type StringOrStringArray = string | string[];
type RequestKeywordDenylist = {
key: string;
value: any;
};
export interface ParseServerOptions {
appId: string;
masterKey: (() => void) | string;
masterKeyTtl?: number;
maintenanceKey: string;
serverURL: string;
masterKeyIps?: (string[]);
maintenanceKeyIps?: (string[]);
appName?: string;
allowHeaders?: (string[]);
allowOrigin?: StringOrStringArray;
analyticsAdapter?: Adapter<AnalyticsAdapter>;
filesAdapter?: Adapter<FilesAdapter>;
push?: any;
scheduledPush?: boolean;
loggerAdapter?: Adapter<LoggerAdapter>;
jsonLogs?: boolean;
logsFolder?: string;
verbose?: boolean;
logLevel?: string;
logLevels?: LogLevels;
maxLogFiles?: NumberOrString;
silent?: boolean;
databaseURI: string;
databaseOptions?: DatabaseOptions;
databaseAdapter?: Adapter<StorageAdapter>;
enableCollationCaseComparison?: boolean;
convertEmailToLowercase?: boolean;
convertUsernameToLowercase?: boolean;
cloud?: string;
collectionPrefix?: string;
clientKey?: string;
javascriptKey?: string;
dotNetKey?: string;
encryptionKey?: string;
restAPIKey?: string;
readOnlyMasterKey?: string;
webhookKey?: string;
fileKey?: string;
preserveFileName?: boolean;
userSensitiveFields?: (string[]);
protectedFields?: ProtectedFields;
enableAnonymousUsers?: boolean;
allowClientClassCreation?: boolean;
allowCustomObjectId?: boolean;
auth?: Record<string, AuthAdapter>;
enableInsecureAuthAdapters?: boolean;
maxUploadSize?: string;
verifyUserEmails?: (boolean | void);
preventLoginWithUnverifiedEmail?: boolean;
preventSignupWithUnverifiedEmail?: boolean;
emailVerifyTokenValidityDuration?: number;
emailVerifyTokenReuseIfValid?: boolean;
sendUserEmailVerification?: (boolean | void);
accountLockout?: AccountLockoutOptions;
passwordPolicy?: PasswordPolicyOptions;
cacheAdapter?: Adapter<CacheAdapter>;
emailAdapter?: Adapter<MailAdapter>;
encodeParseObjectInCloudFunction?: boolean;
publicServerURL?: string;
pages?: PagesOptions;
customPages?: CustomPagesOptions;
liveQuery?: LiveQueryOptions;
sessionLength?: number;
extendSessionOnUse?: boolean;
defaultLimit?: number;
maxLimit?: number;
expireInactiveSessions?: boolean;
revokeSessionOnPasswordReset?: boolean;
cacheTTL?: number;
cacheMaxSize?: number;
directAccess?: boolean;
enableExpressErrorHandler?: boolean;
objectIdSize?: number;
port?: number;
host?: string;
mountPath?: string;
cluster?: NumberOrBoolean;
middleware?: ((() => void) | string);
trustProxy?: any;
startLiveQueryServer?: boolean;
liveQueryServerOptions?: LiveQueryServerOptions;
idempotencyOptions?: IdempotencyOptions;
fileUpload?: FileUploadOptions;
graphQLSchema?: string;
mountGraphQL?: boolean;
graphQLPath?: string;
mountPlayground?: boolean;
playgroundPath?: string;
schema?: SchemaOptions;
serverCloseComplete?: () => void;
security?: SecurityOptions;
enforcePrivateUsers?: boolean;
allowExpiredAuthDataToken?: boolean;
requestKeywordDenylist?: (RequestKeywordDenylist[]);
rateLimit?: (RateLimitOptions[]);
}
export interface RateLimitOptions {
requestPath: string;
requestTimeWindow?: number;
requestCount?: number;
errorResponseMessage?: string;
requestMethods?: (string[]);
includeMasterKey?: boolean;
includeInternalRequests?: boolean;
redisUrl?: string;
zone?: string;
}
export interface SecurityOptions {
enableCheck?: boolean;
enableCheckLog?: boolean;
checkGroups?: (CheckGroup[]);
}
export interface PagesOptions {
enableRouter?: boolean;
enableLocalization?: boolean;
localizationJsonPath?: string;
localizationFallbackLocale?: string;
placeholders?: any;
forceRedirect?: boolean;
pagesPath?: string;
pagesEndpoint?: string;
customUrls?: PagesCustomUrlsOptions;
customRoutes?: (PagesRoute[]);
}
export interface PagesRoute {
path: string;
method: string;
handler: () => void;
}
export interface PagesCustomUrlsOptions {
passwordReset?: string;
passwordResetLinkInvalid?: string;
passwordResetSuccess?: string;
emailVerificationSuccess?: string;
emailVerificationSendFail?: string;
emailVerificationSendSuccess?: string;
emailVerificationLinkInvalid?: string;
emailVerificationLinkExpired?: string;
}
export interface CustomPagesOptions {
invalidLink?: string;
linkSendFail?: string;
choosePassword?: string;
linkSendSuccess?: string;
verifyEmailSuccess?: string;
passwordResetSuccess?: string;
invalidVerificationLink?: string;
expiredVerificationLink?: string;
invalidPasswordResetLink?: string;
parseFrameURL?: string;
}
export interface LiveQueryOptions {
classNames?: (string[]);
redisOptions?: any;
redisURL?: string;
pubSubAdapter?: Adapter<PubSubAdapter>;
wssAdapter?: Adapter<WSSAdapter>;
}
export interface LiveQueryServerOptions {
appId?: string;
masterKey?: string;
serverURL?: string;
keyPairs?: any;
websocketTimeout?: number;
cacheTimeout?: number;
logLevel?: string;
port?: number;
redisOptions?: any;
redisURL?: string;
pubSubAdapter?: Adapter<PubSubAdapter>;
wssAdapter?: Adapter<WSSAdapter>;
}
export interface IdempotencyOptions {
paths?: (string[]);
ttl?: number;
}
export interface AccountLockoutOptions {
duration?: number;
threshold?: number;
unlockOnPasswordReset?: boolean;
}
export interface PasswordPolicyOptions {
validatorPattern?: string;
validatorCallback?: () => void;
validationError?: string;
doNotAllowUsername?: boolean;
maxPasswordAge?: number;
maxPasswordHistory?: number;
resetTokenValidityDuration?: number;
resetTokenReuseIfValid?: boolean;
resetPasswordSuccessOnInvalidEmail?: boolean;
}
export interface FileUploadOptions {
fileExtensions?: (string[]);
enableForAnonymousUser?: boolean;
enableForAuthenticatedUser?: boolean;
enableForPublic?: boolean;
}
export interface DatabaseOptions {
enableSchemaHooks?: boolean;
schemaCacheTtl?: number;
retryWrites?: boolean;
maxTimeMS?: number;
maxStalenessSeconds?: number;
minPoolSize?: number;
maxPoolSize?: number;
connectTimeoutMS?: number;
socketTimeoutMS?: number;
autoSelectFamily?: boolean;
autoSelectFamilyAttemptTimeout?: number;
}
export interface AuthAdapter {
enabled?: boolean;
}
export interface LogLevels {
triggerAfter?: string;
triggerBeforeSuccess?: string;
triggerBeforeError?: string;
cloudFunctionSuccess?: string;
cloudFunctionError?: string;
}
export {};

60
types/ParseServer.d.ts vendored Normal file
View File

@@ -0,0 +1,60 @@
import { ParseServerOptions, LiveQueryServerOptions } from './Options';
import { ParseLiveQueryServer } from './LiveQuery/ParseLiveQueryServer';
declare class ParseServer {
_app: any;
config: any;
server: any;
expressApp: any;
liveQueryServer: any;
/**
* @constructor
* @param {ParseServerOptions} options the parse server initialization options
*/
constructor(options: ParseServerOptions);
/**
* Starts Parse Server as an express app; this promise resolves when Parse Server is ready to accept requests.
*/
start(): Promise<this>;
get app(): any;
/**
* Stops the parse server, cancels any ongoing requests and closes all connections.
*
* Currently, express doesn't shut down immediately after receiving SIGINT/SIGTERM
* if it has client connections that haven't timed out.
* (This is a known issue with node - https://github.com/nodejs/node/issues/2642)
*
* @returns {Promise<void>} a promise that resolves when the server is stopped
*/
handleShutdown(): Promise<void>;
/**
* @static
* Create an express app for the parse server
* @param {Object} options let you specify the maxUploadSize when creating the express app */
static app(options: any): any;
static promiseRouter({ appId }: {
appId: any;
}): any;
/**
* starts the parse server's express app
* @param {ParseServerOptions} options to use to start the server
* @returns {ParseServer} the parse server instance
*/
startApp(options: ParseServerOptions): Promise<this>;
/**
* Creates a new ParseServer and starts it.
* @param {ParseServerOptions} options used to start the server
* @returns {ParseServer} the parse server instance
*/
static startApp(options: ParseServerOptions): Promise<ParseServer>;
/**
* Helper method to create a liveQuery server
* @static
* @param {Server} httpServer an optional http server to pass
* @param {LiveQueryServerOptions} config options for the liveQueryServer
* @param {ParseServerOptions} options options for the ParseServer
* @returns {Promise<ParseLiveQueryServer>} the live query server instance
*/
static createLiveQueryServer(httpServer: any, config: LiveQueryServerOptions, options: ParseServerOptions): Promise<ParseLiveQueryServer>;
static verifyServerUrl(): any;
}
export default ParseServer;

30
types/eslint.config.mjs Normal file
View File

@@ -0,0 +1,30 @@
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import expectType from 'eslint-plugin-expect-type/configs/recommended';
export default tseslint.config({
files: ['**/*.js', '**/*.ts'],
extends: [
expectType,
eslint.configs.recommended,
...tseslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
],
plugins: {
'@typescript-eslint': tseslint.plugin,
},
rules: {
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unsafe-return": "off",
},
languageOptions: {
parser: tseslint.parser,
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
});

21
types/index.d.ts vendored
View File

@@ -0,0 +1,21 @@
import ParseServer from './ParseServer';
import FileSystemAdapter from '@parse/fs-files-adapter';
import InMemoryCacheAdapter from './Adapters/Cache/InMemoryCacheAdapter';
import NullCacheAdapter from './Adapters/Cache/NullCacheAdapter';
import RedisCacheAdapter from './Adapters/Cache/RedisCacheAdapter';
import LRUCacheAdapter from './Adapters/Cache/LRUCache.js';
import * as TestUtils from './TestUtils';
import * as SchemaMigrations from './SchemaMigrations/Migrations';
import AuthAdapter from './Adapters/Auth/AuthAdapter';
import { PushWorker } from './Push/PushWorker';
import { ParseServerOptions } from './Options';
import { ParseGraphQLServer } from './GraphQL/ParseGraphQLServer';
declare const _ParseServer: {
(options: ParseServerOptions): ParseServer;
createLiveQueryServer: typeof ParseServer.createLiveQueryServer;
startApp: typeof ParseServer.startApp;
};
declare const S3Adapter: any;
declare const GCSAdapter: any;
export default ParseServer;
export { S3Adapter, GCSAdapter, FileSystemAdapter, InMemoryCacheAdapter, NullCacheAdapter, RedisCacheAdapter, LRUCacheAdapter, TestUtils, PushWorker, ParseGraphQLServer, _ParseServer as ParseServer, SchemaMigrations, AuthAdapter, };

44
types/tests.ts Normal file
View File

@@ -0,0 +1,44 @@
import ParseServer, { FileSystemAdapter } from 'parse-server';
async function server() {
// $ExpectType ParseServer
const parseServer = await ParseServer.startApp({});
// $ExpectType void
await parseServer.handleShutdown();
// $ExpectType any
parseServer.app;
// $ExpectType any
ParseServer.app({});
// $ExpectType any
ParseServer.promiseRouter({ appId: 'appId' });
// $ExpectType ParseLiveQueryServer
await ParseServer.createLiveQueryServer({}, {}, {});
// $ExpectType any
ParseServer.verifyServerUrl();
// $ExpectError
await ParseServer.startApp();
// $ExpectError
ParseServer.promiseRouter();
// $ExpectError
await ParseServer.createLiveQueryServer();
// $ExpectType ParseServer
const parseServer2 = new ParseServer({});
// $ExpectType ParseServer
await parseServer2.start();
}
function exports() {
// $ExpectType any
FileSystemAdapter;
}

View File

@@ -13,6 +13,12 @@
// If the library is an external module (uses `export`), this allows your test file to import "mylib" instead of "./index". // If the library is an external module (uses `export`), this allows your test file to import "mylib" instead of "./index".
// If the library is global (cannot be imported via `import` or `require`), leave this out. // If the library is global (cannot be imported via `import` or `require`), leave this out.
"baseUrl": ".", "baseUrl": ".",
"paths": { "parse": ["."] } "paths": {
"parse-server": ["."],
"@parse/fs-files-adapter": ["./@types/@parse/fs-files-adapter"],
} }
},
"include": [
"tests.ts"
]
} }