diff --git a/package-lock.json b/package-lock.json index 9e4808191..29df00318 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "@semantic-release/github": "8.0.7", "@semantic-release/npm": "9.0.2", "@semantic-release/release-notes-generator": "10.0.3", + "@types/facebook-js-sdk": "3.3.9", "babel-jest": "29.5.0", "babel-plugin-inline-package-json": "2.0.0", "babel-plugin-minify-dead-code-elimination": "0.5.2", @@ -5049,6 +5050,12 @@ "@types/range-parser": "*" } }, + "node_modules/@types/facebook-js-sdk": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/@types/facebook-js-sdk/-/facebook-js-sdk-3.3.9.tgz", + "integrity": "sha512-uJiJ+ljEPzC7jHGXl8YT7gRUh0fGzzJYrdwyrjgwSqFvrcCwlWMu/nLLcJeIRoFA81uVBwZBOKQIkjXFknXPsA==", + "dev": true + }, "node_modules/@types/graceful-fs": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", @@ -31544,6 +31551,12 @@ "@types/range-parser": "*" } }, + "@types/facebook-js-sdk": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/@types/facebook-js-sdk/-/facebook-js-sdk-3.3.9.tgz", + "integrity": "sha512-uJiJ+ljEPzC7jHGXl8YT7gRUh0fGzzJYrdwyrjgwSqFvrcCwlWMu/nLLcJeIRoFA81uVBwZBOKQIkjXFknXPsA==", + "dev": true + }, "@types/graceful-fs": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", diff --git a/package.json b/package.json index 22a92f152..12654f893 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@semantic-release/github": "8.0.7", "@semantic-release/npm": "9.0.2", "@semantic-release/release-notes-generator": "10.0.3", + "@types/facebook-js-sdk": "3.3.9", "babel-jest": "29.5.0", "babel-plugin-inline-package-json": "2.0.0", "babel-plugin-minify-dead-code-elimination": "0.5.2", diff --git a/src/Analytics.js b/src/Analytics.ts similarity index 95% rename from src/Analytics.js rename to src/Analytics.ts index 32f4cf82f..3a6b367c3 100644 --- a/src/Analytics.js +++ b/src/Analytics.ts @@ -44,7 +44,7 @@ import CoreManager from './CoreManager'; * @returns {Promise} A promise that is resolved when the round-trip * to the server completes. */ -export function track(name: string, dimensions: { [key: string]: string }): Promise { +export function track(name: string, dimensions: { [key: string]: string }): Promise { name = name || ''; name = name.replace(/^\s*/, ''); name = name.replace(/\s*$/, ''); @@ -62,7 +62,7 @@ export function track(name: string, dimensions: { [key: string]: string }): Prom } const DefaultController = { - track(name, dimensions) { + track(name: string, dimensions: { [key: string]: string }) { const path = 'events/' + name; const RESTController = CoreManager.getRESTController(); return RESTController.request('POST', path, { dimensions: dimensions }); diff --git a/src/AnonymousUtils.js b/src/AnonymousUtils.ts similarity index 99% rename from src/AnonymousUtils.js rename to src/AnonymousUtils.ts index 2472f8b7d..9950af8dd 100644 --- a/src/AnonymousUtils.js +++ b/src/AnonymousUtils.ts @@ -4,7 +4,7 @@ import ParseUser from './ParseUser'; import type { RequestOptions } from './RESTController'; -const uuidv4 = require('./uuid'); +import uuidv4 from './uuid'; let registered = false; diff --git a/src/Cloud.js b/src/Cloud.ts similarity index 92% rename from src/Cloud.js rename to src/Cloud.ts index 0d218843c..7dcf76a00 100644 --- a/src/Cloud.js +++ b/src/Cloud.ts @@ -33,14 +33,14 @@ import type { RequestOptions } from './RESTController'; * @returns {Promise} A promise that will be resolved with the result * of the function. */ -export function run(name: string, data: mixed, options: RequestOptions): Promise { +export function run(name: string, data: any, options: RequestOptions): Promise { options = options || {}; if (typeof name !== 'string' || name.length === 0) { throw new TypeError('Cloud function name must be a string.'); } - const requestOptions = {}; + const requestOptions: RequestOptions = {}; if (options.useMasterKey) { requestOptions.useMasterKey = options.useMasterKey; } @@ -82,7 +82,7 @@ export function getJobsData(): Promise { * @returns {Promise} A promise that will be resolved with the jobStatusId * of the job. */ -export function startJob(name: string, data: mixed): Promise { +export function startJob(name: string, data: any): Promise { if (typeof name !== 'string' || name.length === 0) { throw new TypeError('Cloud job name must be a string.'); } @@ -106,7 +106,7 @@ export function getJobStatus(jobStatusId: string): Promise { } const DefaultController = { - run(name, data, options: RequestOptions) { + run(name: string, data: any, options: RequestOptions) { const RESTController = CoreManager.getRESTController(); const payload = encode(data, true); @@ -131,7 +131,7 @@ const DefaultController = { return RESTController.request('GET', 'cloud_code/jobs/data', null, options); }, - async startJob(name, data, options: RequestOptions) { + async startJob(name: string, data: any, options: RequestOptions): Promise { const RESTController = CoreManager.getRESTController(); const payload = encode(data, true); diff --git a/src/CloudCode.js b/src/CloudCode.ts similarity index 100% rename from src/CloudCode.js rename to src/CloudCode.ts diff --git a/src/CoreManager.js b/src/CoreManager.ts similarity index 54% rename from src/CoreManager.js rename to src/CoreManager.ts index 60467e2a6..9c8325683 100644 --- a/src/CoreManager.js +++ b/src/CoreManager.ts @@ -1,10 +1,6 @@ -/* - * @flow - */ - import type { AttributeMap, ObjectCache, OpsMap, State } from './ObjectStateMutations'; import type ParseFile from './ParseFile'; -import type { FileSource } from './ParseFile'; +import type { FileSaveOptions, FileSource } from './ParseFile'; import type { Op } from './ParseOp'; import type ParseObject from './ParseObject'; import type { QueryJSON } from './ParseQuery'; @@ -12,139 +8,201 @@ import type ParseUser from './ParseUser'; import type { AuthData } from './ParseUser'; import type { PushData } from './Push'; import type { RequestOptions, FullOptions } from './RESTController'; +import type ParseSession from './ParseSession'; +import type { HookDeclaration, HookDeleteArg } from './ParseHooks'; +import type ParseConfig from './ParseConfig'; +import type LiveQueryClient from './LiveQueryClient'; +import type ParseSchema from './ParseSchema'; +import type { StorageController } from './Storage'; type AnalyticsController = { - track: (name: string, dimensions: { [key: string]: string }) => Promise, + track: (name: string, dimensions: { [key: string]: string }) => Promise, }; type CloudController = { - run: (name: string, data: mixed, options: RequestOptions) => Promise, - getJobsData: (options: RequestOptions) => Promise, - startJob: (name: string, data: mixed, options: RequestOptions) => Promise, + run: (name: string, data: any, options: RequestOptions) => Promise, + getJobsData: (options: RequestOptions) => Promise, + /** Returns promise which resolves with JobStatusId of the job */ + startJob: (name: string, data: any, options: RequestOptions) => Promise, }; type ConfigController = { - current: () => Promise, - get: () => Promise, - save: (attrs: { [key: string]: any }) => Promise, + current: () => Promise | ParseConfig, + get: (opts?: RequestOptions) => Promise, + save: (attrs: { [key: string]: any }, masterKeyOnlyFlags?: { [key: string]: any }) => Promise, }; type CryptoController = { encrypt: (obj: any, secretKey: string) => string, decrypt: (encryptedText: string, secretKey: any) => string, }; type FileController = { - saveFile: (name: string, source: FileSource, options: FullOptions) => Promise, - saveBase64: (name: string, source: FileSource, options: FullOptions) => Promise, - download: (uri: string) => Promise, + saveFile: (name: string, source: FileSource, options?: FullOptions) => Promise, + saveBase64: (name: string, source: FileSource, options?: FileSaveOptions) => Promise<{ name: string, url: string }>, + download: (uri: string, options?: any) => Promise<{ base64?: string, contentType?: string }>, + deleteFile: (name: string, options?: { useMasterKey?: boolean }) => Promise, }; type InstallationController = { - currentInstallationId: () => Promise, + currentInstallationId: () => Promise, }; type ObjectController = { fetch: ( object: ParseObject | Array, forceFetch: boolean, options: RequestOptions - ) => Promise, - save: (object: ParseObject | Array, options: RequestOptions) => Promise, - destroy: (object: ParseObject | Array, options: RequestOptions) => Promise, + ) => Promise, + save: (object: ParseObject | Array | null, options: RequestOptions) => Promise | ParseFile>, + destroy: (object: ParseObject | Array, options: RequestOptions) => Promise>, }; type ObjectStateController = { - getState: (obj: any) => ?State, + getState: (obj: any) => State | null, initializeState: (obj: any, initial?: State) => State, - removeState: (obj: any) => ?State, + removeState: (obj: any) => State | null, getServerData: (obj: any) => AttributeMap, setServerData: (obj: any, attributes: AttributeMap) => void, getPendingOps: (obj: any) => Array, - setPendingOp: (obj: any, attr: string, op: ?Op) => void, + setPendingOp: (obj: any, attr: string, op?: Op) => void, pushPendingState: (obj: any) => void, - popPendingState: (obj: any) => OpsMap, + popPendingState: (obj: any) => OpsMap | undefined, mergeFirstPendingState: (obj: any) => void, getObjectCache: (obj: any) => ObjectCache, - estimateAttribute: (obj: any, attr: string) => mixed, + estimateAttribute: (obj: any, attr: string) => any, estimateAttributes: (obj: any) => AttributeMap, commitServerChanges: (obj: any, changes: AttributeMap) => void, - enqueueTask: (obj: any, task: () => Promise) => Promise, + enqueueTask: (obj: any, task: () => Promise) => Promise, clearAllState: () => void, duplicateState: (source: any, dest: any) => void, }; type PushController = { - send: (data: PushData) => Promise, + send: (data: PushData, options?: FullOptions) => Promise, }; type QueryController = { - find: (className: string, params: QueryJSON, options: RequestOptions) => Promise, - aggregate: (className: string, params: any, options: RequestOptions) => Promise, + find(className: string, params: QueryJSON, options: RequestOptions): Promise<{ results?: Array, className?: string, count?: number }>; + aggregate(className: string, params: any, options: RequestOptions): Promise<{ results?: Array }>; }; type RESTController = { - request: (method: string, path: string, data: mixed, options: RequestOptions) => Promise, - ajax: (method: string, url: string, data: any, headers?: any, options: FullOptions) => Promise, + request: (method: string, path: string, data?: any, options?: RequestOptions) => Promise, + ajax: (method: string, url: string, data: any, headers?: any, options?: FullOptions) => Promise, + handleError: (err?: any) => void, }; type SchemaController = { - purge: (className: string) => Promise, - get: (className: string, options: RequestOptions) => Promise, - delete: (className: string, options: RequestOptions) => Promise, - create: (className: string, params: any, options: RequestOptions) => Promise, - update: (className: string, params: any, options: RequestOptions) => Promise, - send(className: string, method: string, params: any, options: RequestOptions): Promise, + purge: (className: string) => Promise, + get: (className: string, options?: RequestOptions) => Promise<{ results: ParseSchema[] }>, + delete: (className: string, options?: RequestOptions) => Promise, + create: (className: string, params: any, options?: RequestOptions) => Promise, + update: (className: string, params: any, options?: RequestOptions) => Promise, + send(className: string, method: string, params: any, options: RequestOptions): Promise, }; type SessionController = { - getSession: (token: RequestOptions) => Promise, + getSession: (token: RequestOptions) => Promise, }; -type StorageController = - | { - async: 0, - getItem: (path: string) => ?string, - setItem: (path: string, value: string) => void, - removeItem: (path: string) => void, - getItemAsync?: (path: string) => Promise, - setItemAsync?: (path: string, value: string) => Promise, - removeItemAsync?: (path: string) => Promise, - clear: () => void, - } - | { - async: 1, - getItem?: (path: string) => ?string, - setItem?: (path: string, value: string) => void, - removeItem?: (path: string) => void, - getItemAsync: (path: string) => Promise, - setItemAsync: (path: string, value: string) => Promise, - removeItemAsync: (path: string) => Promise, - clear: () => void, - }; type LocalDatastoreController = { - fromPinWithName: (name: string) => ?any, + fromPinWithName: (name: string) => any | undefined, pinWithName: (name: string, objects: any) => void, unPinWithName: (name: string) => void, - getAllContents: () => ?any, + getAllContents: () => any | undefined, clear: () => void, + // Use for testing + // getRawStorage(): Promise, }; type UserController = { - setCurrentUser: (user: ParseUser) => Promise, - currentUser: () => ?ParseUser, - currentUserAsync: () => Promise, - signUp: (user: ParseUser, attrs: AttributeMap, options: RequestOptions) => Promise, - logIn: (user: ParseUser, options: RequestOptions) => Promise, - become: (options: RequestOptions) => Promise, - hydrate: (userJSON: AttributeMap) => Promise, - logOut: (options: RequestOptions) => Promise, - me: (options: RequestOptions) => Promise, - requestPasswordReset: (email: string, options: RequestOptions) => Promise, - updateUserOnDisk: (user: ParseUser) => Promise, - upgradeToRevocableSession: (user: ParseUser, options: RequestOptions) => Promise, - linkWith: (user: ParseUser, authData: AuthData) => Promise, - removeUserFromDisk: () => Promise, - verifyPassword: (username: string, password: string, options: RequestOptions) => Promise, - requestEmailVerification: (email: string, options: RequestOptions) => Promise, + setCurrentUser: (user: ParseUser) => Promise, + currentUser: () => ParseUser | null, + currentUserAsync: () => Promise, + signUp: (user: ParseUser, attrs: AttributeMap, options: RequestOptions) => Promise, + logIn: (user: ParseUser, options: RequestOptions) => Promise, + loginAs: (user: ParseUser, userId: string) => Promise, + become: (user: ParseUser, options: RequestOptions) => Promise, + hydrate: (user: ParseUser, userJSON: AttributeMap) => Promise, + logOut: (options: RequestOptions) => Promise, + me: (user: ParseUser, options: RequestOptions) => Promise, + requestPasswordReset: (email: string, options: RequestOptions) => Promise, + updateUserOnDisk: (user: ParseUser) => Promise, + upgradeToRevocableSession: (user: ParseUser, options: RequestOptions) => Promise, + linkWith: (user: ParseUser, authData: AuthData, options?: FullOptions) => Promise, + removeUserFromDisk: () => Promise, + verifyPassword: (username: string, password: string, options: RequestOptions) => Promise, + requestEmailVerification: (email: string, options: RequestOptions) => Promise, }; type HooksController = { - get: (type: string, functionName?: string, triggerName?: string) => Promise, - create: (hook: mixed) => Promise, - delete: (hook: mixed) => Promise, - update: (hook: mixed) => Promise, - send: (method: string, path: string, body?: mixed) => Promise, + get: (type: string, functionName?: string, triggerName?: string) => Promise, + create: (hook: HookDeclaration) => Promise, + remove: (hook: HookDeleteArg) => Promise, + update: (hook: HookDeclaration) => Promise, + // Renamed to sendRequest since ParseHooks file & tests file uses this. (originally declared as just "send") + sendRequest?: (method: string, path: string, body?: any) => Promise, +}; +type LiveQueryControllerType = { + setDefaultLiveQueryClient(liveQueryClient: LiveQueryClient): void; + getDefaultLiveQueryClient(): Promise; + _clearCachedDefaultClient(): void; +} +/** Based on https://github.com/react-native-async-storage/async-storage/blob/main/packages/default-storage-backend/src/types.ts */ +type AsyncStorageType = { + /** Fetches an item for a `key` and invokes a callback upon completion. */ + getItem: ( + key: string, + callback?: (error?: Error | null, result?: string | null) => void + ) => Promise; + /** Sets the value for a `key` and invokes a callback upon completion. */ + setItem: (key: string, value: string, callback?: (error?: Error | null) => void) => Promise; + /** Removes an item for a `key` and invokes a callback upon completion. */ + removeItem: (key: string, callback?: (error?: Error | null) => void) => Promise; + /** Merges an existing `key` value with an input value, assuming both values are stringified JSON. */ + mergeItem: (key: string, value: string, callback?: (error?: Error | null) => void) => Promise; + /** + * Erases *all* `AsyncStorage` for all clients, libraries, etc. You probably + * don't want to call this; use `removeItem` or `multiRemove` to clear only + * your app's keys. + */ + clear: (callback?: (error?: Error | null) => void) => Promise; + /** Gets *all* keys known to your app; for all callers, libraries, etc. */ + getAllKeys: ( + callback?: (error?: Error | null, result?: readonly string[] | null) => void + ) => Promise; + /** + * This allows you to batch the fetching of items given an array of `key` + * inputs. Your callback will be invoked with an array of corresponding + * key-value pairs found. + */ + multiGet: ( + keys: readonly string[], + callback?: (errors?: readonly (Error | null)[] | null, result?: readonly [string, string][]) => void + ) => Promise; + + /** + * Use this as a batch operation for storing multiple key-value pairs. When + * the operation completes you'll get a single callback with any errors. + * + * See https://react-native-async-storage.github.io/async-storage/docs/api#multiset + */ + multiSet: ( + keyValuePairs: [string, string][], + callback?: (errors?: readonly (Error | null)[] | null) => void + ) => Promise; + + /** + * Call this to batch the deletion of all keys in the `keys` array. + * + * See https://react-native-async-storage.github.io/async-storage/docs/api#multiremove + */ + multiRemove: ( + keys: readonly string[], + callback?: (errors?: readonly (Error | null)[] | null) => void + ) => Promise; + + /** + * Batch operation to merge in existing and new values for a given set of + * keys. This assumes that the values are stringified JSON. + * + * See https://react-native-async-storage.github.io/async-storage/docs/api#multimerge + */ + multiMerge: ( + keyValuePairs: [string, string][], + callback?: (errors?: readonly (Error | null)[] | null) => void + ) => Promise; }; -type WebSocketController = { +export type WebSocketController = { onopen: () => void, onmessage: (message: any) => void, - onclose: () => void, + onclose: (arg?: any) => void, onerror: (error: any) => void, send: (data: any) => void, close: () => void, @@ -166,11 +224,12 @@ type Config = { LocalDatastoreController?: LocalDatastoreController, UserController?: UserController, HooksController?: HooksController, - WebSocketController?: WebSocketController, + WebSocketController?: new (url: string | URL, protocols?: string | string[] | undefined) => WebSocketController, + LiveQueryController?: LiveQueryControllerType, + AsyncStorage?: AsyncStorageType }; -const config: Config & { [key: string]: mixed } = { - // Defaults +const config: Config & { [key: string]: any } = { IS_NODE: typeof process !== 'undefined' && !!process.versions && @@ -232,7 +291,7 @@ const CoreManager = { }, getAnalyticsController(): AnalyticsController { - return config['AnalyticsController']; + return config['AnalyticsController']!; }, setCloudController(controller: CloudController) { @@ -241,7 +300,7 @@ const CoreManager = { }, getCloudController(): CloudController { - return config['CloudController']; + return config['CloudController']!; }, setConfigController(controller: ConfigController) { @@ -250,7 +309,7 @@ const CoreManager = { }, getConfigController(): ConfigController { - return config['ConfigController']; + return config['ConfigController']!; }, setCryptoController(controller: CryptoController) { @@ -276,7 +335,7 @@ const CoreManager = { }, getFileController(): FileController { - return config['FileController']; + return config['FileController']!; }, setInstallationController(controller: InstallationController) { @@ -285,7 +344,7 @@ const CoreManager = { }, getInstallationController(): InstallationController { - return config['InstallationController']; + return config['InstallationController']!; }, setLiveQuery(liveQuery: any) { @@ -302,7 +361,7 @@ const CoreManager = { }, getObjectController(): ObjectController { - return config['ObjectController']; + return config['ObjectController']!; }, setObjectStateController(controller: ObjectStateController) { @@ -333,7 +392,7 @@ const CoreManager = { }, getObjectStateController(): ObjectStateController { - return config['ObjectStateController']; + return config['ObjectStateController']!; }, setPushController(controller: PushController) { @@ -342,7 +401,7 @@ const CoreManager = { }, getPushController(): PushController { - return config['PushController']; + return config['PushController']!; }, setQueryController(controller: QueryController) { @@ -351,7 +410,7 @@ const CoreManager = { }, getQueryController(): QueryController { - return config['QueryController']; + return config['QueryController']!; }, setRESTController(controller: RESTController) { @@ -360,7 +419,7 @@ const CoreManager = { }, getRESTController(): RESTController { - return config['RESTController']; + return config['RESTController']!; }, setSchemaController(controller: SchemaController) { @@ -373,7 +432,7 @@ const CoreManager = { }, getSchemaController(): SchemaController { - return config['SchemaController']; + return config['SchemaController']!; }, setSessionController(controller: SessionController) { @@ -382,7 +441,7 @@ const CoreManager = { }, getSessionController(): SessionController { - return config['SessionController']; + return config['SessionController']!; }, setStorageController(controller: StorageController) { @@ -412,7 +471,7 @@ const CoreManager = { }, getLocalDatastoreController(): LocalDatastoreController { - return config['LocalDatastoreController']; + return config['LocalDatastoreController']!; }, setLocalDatastore(store: any) { @@ -424,10 +483,10 @@ const CoreManager = { }, getStorageController(): StorageController { - return config['StorageController']; + return config['StorageController']!; }, - setAsyncStorage(storage: any) { + setAsyncStorage(storage: AsyncStorageType) { config['AsyncStorage'] = storage; }, @@ -435,12 +494,12 @@ const CoreManager = { return config['AsyncStorage']; }, - setWebSocketController(controller: WebSocketController) { + setWebSocketController(controller: new (url: string | URL, protocols?: string | string[] | undefined) => WebSocketController) { config['WebSocketController'] = controller; }, - getWebSocketController(): WebSocketController { - return config['WebSocketController']; + getWebSocketController(): new (url: string | URL, protocols?: string | string[] | undefined) => WebSocketController { + return config['WebSocketController']!; }, setUserController(controller: UserController) { @@ -467,10 +526,10 @@ const CoreManager = { }, getUserController(): UserController { - return config['UserController']; + return config['UserController']!; }, - setLiveQueryController(controller: any) { + setLiveQueryController(controller: LiveQueryControllerType) { requireMethods( 'LiveQueryController', ['setDefaultLiveQueryClient', 'getDefaultLiveQueryClient', '_clearCachedDefaultClient'], @@ -479,8 +538,8 @@ const CoreManager = { config['LiveQueryController'] = controller; }, - getLiveQueryController(): any { - return config['LiveQueryController']; + getLiveQueryController(): LiveQueryControllerType { + return config['LiveQueryController']!; }, setHooksController(controller: HooksController) { @@ -489,7 +548,7 @@ const CoreManager = { }, getHooksController(): HooksController { - return config['HooksController']; + return config['HooksController']!; }, }; diff --git a/src/CryptoController.js b/src/CryptoController.ts similarity index 78% rename from src/CryptoController.js rename to src/CryptoController.ts index 49b7fc725..fd27a4bd2 100644 --- a/src/CryptoController.js +++ b/src/CryptoController.ts @@ -11,15 +11,16 @@ if (process.env.PARSE_BUILD === 'react-native') { } const CryptoController = { - encrypt(obj: any, secretKey: string): ?string { + encrypt(obj: any, secretKey: string): string { const encrypted = AES.encrypt(JSON.stringify(obj), secretKey); return encrypted.toString(); }, - decrypt(encryptedText: string, secretKey: string): ?string { + decrypt(encryptedText: string, secretKey: string): string { const decryptedStr = AES.decrypt(encryptedText, secretKey).toString(ENC); return decryptedStr; }, }; module.exports = CryptoController; +export default CryptoController; diff --git a/src/EventEmitter.js b/src/EventEmitter.js deleted file mode 100644 index 1f1bcbd00..000000000 --- a/src/EventEmitter.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * This is a simple wrapper to unify EventEmitter implementations across platforms. - */ - -let EventEmitter; - -try { - if (process.env.PARSE_BUILD === 'react-native') { - EventEmitter = require('react-native/Libraries/vendor/emitter/EventEmitter'); - if (EventEmitter.default) { - EventEmitter = EventEmitter.default; - } - EventEmitter.prototype.on = EventEmitter.prototype.addListener; - } else { - EventEmitter = require('events').EventEmitter; - } -} catch (_) { - // EventEmitter unavailable -} -module.exports = EventEmitter; diff --git a/src/EventEmitter.ts b/src/EventEmitter.ts new file mode 100644 index 000000000..205baf4c0 --- /dev/null +++ b/src/EventEmitter.ts @@ -0,0 +1,25 @@ +import type { EventEmitter as EventEmitterType } from 'events'; +/** + * This is a simple wrapper to unify EventEmitter implementations across platforms. + */ + +let EventEmitter: typeof EventEmitterType; + +try { + if (process.env.PARSE_BUILD === 'react-native') { + let RNEventEmitter = require('react-native/Libraries/vendor/emitter/EventEmitter'); + if (RNEventEmitter.default) { + EventEmitter = RNEventEmitter.default; + } + else { + EventEmitter = RNEventEmitter; + } + (EventEmitter as any).prototype.on = RNEventEmitter.prototype.addListener; + } else { + EventEmitter = require('events').EventEmitter; + } +} catch (_) { + // EventEmitter unavailable +} +module.exports = EventEmitter!; +export default EventEmitter!; diff --git a/src/EventuallyQueue.js b/src/EventuallyQueue.ts similarity index 88% rename from src/EventuallyQueue.js rename to src/EventuallyQueue.ts index 88df1d3bd..53f9f9782 100644 --- a/src/EventuallyQueue.js +++ b/src/EventuallyQueue.ts @@ -27,9 +27,9 @@ type QueueObject = { type Queue = Array; const QUEUE_KEY = 'Parse/Eventually/Queue'; -let queueCache = []; +let queueCache: QueueObject[] = []; let dirtyCache = true; -let polling = undefined; +let polling: ReturnType | undefined = undefined; /** * Provides utility functions to queue objects that will be @@ -50,7 +50,7 @@ const EventuallyQueue = { * @static * @see Parse.Object#saveEventually */ - save(object: ParseObject, serverOptions: SaveOptions = {}): Promise { + save(object: ParseObject, serverOptions: SaveOptions = {}): Promise { return this.enqueue('save', object, serverOptions); }, @@ -65,7 +65,7 @@ const EventuallyQueue = { * @static * @see Parse.Object#destroyEventually */ - destroy(object: ParseObject, serverOptions: RequestOptions = {}): Promise { + destroy(object: ParseObject, serverOptions: RequestOptions = {}): Promise { return this.enqueue('destroy', object, serverOptions); }, @@ -99,7 +99,7 @@ const EventuallyQueue = { action: string, object: ParseObject, serverOptions: SaveOptions | RequestOptions - ): Promise { + ): Promise { const queueData = await this.getQueue(); const queueId = this.generateQueueId(action, object); @@ -127,7 +127,7 @@ const EventuallyQueue = { return this.setQueue(queueData); }, - store(data) { + store(data: QueueObject[]) { return Storage.setItemAsync(QUEUE_KEY, JSON.stringify(data)); }, @@ -143,7 +143,7 @@ const EventuallyQueue = { * @returns {Promise} * @static */ - async getQueue(): Promise { + async getQueue(): Promise { if (dirtyCache) { queueCache = JSON.parse((await this.load()) || '[]'); dirtyCache = false; @@ -189,7 +189,7 @@ const EventuallyQueue = { * @returns {Promise} A promise that is fulfilled when queue is cleared. * @static */ - clear(): Promise { + clear(): Promise { queueCache = []; return this.store([]); }, @@ -215,7 +215,7 @@ const EventuallyQueue = { * @returns {number} * @static */ - async length(): number { + async length(): Promise { const queueData = await this.getQueue(); return queueData.length; }, @@ -264,33 +264,33 @@ const EventuallyQueue = { return this.remove(queueObject.queueId); } switch (queueObject.action) { - case 'save': - // Queued update was overwritten by other request. Do not save - if ( - typeof object.updatedAt !== 'undefined' && - object.updatedAt > new Date(queueObject.object.createdAt) - ) { - return this.remove(queueObject.queueId); - } - try { - await object.save(queueObject.object, queueObject.serverOptions); - await this.remove(queueObject.queueId); - } catch (e) { - if (e.code !== ParseError.CONNECTION_FAILED) { + case 'save': + // Queued update was overwritten by other request. Do not save + if ( + typeof object.updatedAt !== 'undefined' && + object.updatedAt > new Date(queueObject.object.createdAt!) + ) { + return this.remove(queueObject.queueId); + } + try { + await object.save(queueObject.object, queueObject.serverOptions); await this.remove(queueObject.queueId); + } catch (e) { + if (e.code !== ParseError.CONNECTION_FAILED) { + await this.remove(queueObject.queueId); + } } - } - break; - case 'destroy': - try { - await object.destroy(queueObject.serverOptions); - await this.remove(queueObject.queueId); - } catch (e) { - if (e.code !== ParseError.CONNECTION_FAILED) { + break; + case 'destroy': + try { + await object.destroy(queueObject.serverOptions); await this.remove(queueObject.queueId); + } catch (e) { + if (e.code !== ParseError.CONNECTION_FAILED) { + await this.remove(queueObject.queueId); + } } - } - break; + break; } }, @@ -344,16 +344,17 @@ const EventuallyQueue = { return !!polling; }, + /** Used only for haxx debug */ _setPolling(flag: boolean) { - polling = flag; + polling = flag as any; }, process: { - create(ObjectType, queueObject) { + create(ObjectType: typeof ParseObject, queueObject) { const object = new ObjectType(); return EventuallyQueue.sendQueueCallback(object, queueObject); }, - async byId(ObjectType, queueObject) { + async byId(ObjectType: string | ParseObject | (typeof ParseObject), queueObject) { const { sessionToken } = queueObject.serverOptions; const query = new ParseQuery(ObjectType); query.equalTo('objectId', queueObject.id); diff --git a/src/FacebookUtils.js b/src/FacebookUtils.ts similarity index 94% rename from src/FacebookUtils.js rename to src/FacebookUtils.ts index d7ca36b2a..5b744b794 100644 --- a/src/FacebookUtils.js +++ b/src/FacebookUtils.ts @@ -3,11 +3,12 @@ */ /* global FB */ import ParseUser from './ParseUser'; +import type { AuthProviderType } from './ParseUser'; let initialized = false; let requestedPermissions; let initOptions; -const provider = { +const provider: AuthProviderType = { authenticate(options) { if (typeof FB === 'undefined') { options.error(this, 'Facebook SDK not found.'); @@ -38,7 +39,7 @@ const provider = { restoreAuthentication(authData) { if (authData) { - const newOptions = {}; + const newOptions: typeof initOptions = {}; if (initOptions) { for (const key in initOptions) { newOptions[key] = initOptions[key]; @@ -107,13 +108,13 @@ const FacebookUtils = { } } if (initOptions.status && typeof console !== 'undefined') { - const warn = console.warn || console.log || function () {}; // eslint-disable-line no-console + const warn = console.warn || console.log || function () { }; // eslint-disable-line no-console warn.call( console, 'The "status" flag passed into' + - ' FB.init, when set to true, can interfere with Parse Facebook' + - ' integration, so it has been suppressed. Please call' + - ' FB.getLoginStatus() explicitly if you require this behavior.' + ' FB.init, when set to true, can interfere with Parse Facebook' + + ' integration, so it has been suppressed. Please call' + + ' FB.getLoginStatus() explicitly if you require this behavior.' ); } initOptions.status = false; diff --git a/src/IndexedDBStorageController.js b/src/IndexedDBStorageController.ts similarity index 82% rename from src/IndexedDBStorageController.js rename to src/IndexedDBStorageController.ts index da2d2e973..cfb6e1c78 100644 --- a/src/IndexedDBStorageController.js +++ b/src/IndexedDBStorageController.ts @@ -4,12 +4,14 @@ /* global window */ import { createStore, del, set, get, clear, keys } from 'idb-keyval'; +import type { StorageController } from './Storage'; +let IndexedDBStorageController: StorageController; if (typeof window !== 'undefined' && window.indexedDB) { try { const ParseStore = createStore('parseDB', 'parseStore'); - const IndexedDBStorageController = { + IndexedDBStorageController = { async: 1, getItemAsync(path: string) { return get(path, ParseStore); @@ -37,3 +39,4 @@ if (typeof window !== 'undefined' && window.indexedDB) { // IndexedDB not supported module.exports = undefined; } +export default IndexedDBStorageController; diff --git a/src/InstallationController.js b/src/InstallationController.ts similarity index 80% rename from src/InstallationController.js rename to src/InstallationController.ts index 41d865d10..4a5e027ae 100644 --- a/src/InstallationController.js +++ b/src/InstallationController.ts @@ -5,7 +5,7 @@ import Storage from './Storage'; const uuidv4 = require('./uuid'); -let iidCache = null; +let iidCache: string|null = null; const InstallationController = { currentInstallationId(): Promise { @@ -16,9 +16,9 @@ const InstallationController = { return Storage.getItemAsync(path).then(iid => { if (!iid) { iid = uuidv4(); - return Storage.setItemAsync(path, iid).then(() => { + return Storage.setItemAsync(path, iid!).then(() => { iidCache = iid; - return iid; + return iid!; }); } iidCache = iid; @@ -36,3 +36,4 @@ const InstallationController = { }; module.exports = InstallationController; +export default InstallationController; diff --git a/src/LiveQueryClient.js b/src/LiveQueryClient.ts similarity index 75% rename from src/LiveQueryClient.js rename to src/LiveQueryClient.ts index 790624fbf..1e93b3b8e 100644 --- a/src/LiveQueryClient.js +++ b/src/LiveQueryClient.ts @@ -1,10 +1,12 @@ /* global WebSocket */ -import CoreManager from './CoreManager'; +import CoreManager, { WebSocketController } from './CoreManager'; import ParseObject from './ParseObject'; import LiveQuerySubscription from './LiveQuerySubscription'; import { resolvingPromise } from './promiseUtils'; import ParseError from './ParseError'; +import type { EventEmitter } from 'events'; +import type ParseQuery from './ParseQuery'; // The LiveQuery client inner state const CLIENT_STATE = { @@ -109,15 +111,19 @@ class LiveQueryClient { requestId: number; applicationId: string; serverURL: string; - javascriptKey: ?string; - masterKey: ?string; - sessionToken: ?string; - installationId: ?string; + javascriptKey?: string; + masterKey?: string; + sessionToken?: string; + installationId?: string; additionalProperties: boolean; - connectPromise: Promise; - subscriptions: Map; - socket: any; + connectPromise: ReturnType>; + subscriptions: Map; + socket: WebSocketController & { closingPromise?: ReturnType> }; state: string; + reconnectHandle: null | ReturnType = null; + emitter: EventEmitter; + on: EventEmitter['on']; + emit: EventEmitter['emit']; /** * @param {object} options @@ -156,14 +162,14 @@ class LiveQueryClient { this.connectPromise = resolvingPromise(); this.subscriptions = new Map(); this.state = CLIENT_STATE.INITIALIZED; - const EventEmitter = CoreManager.getEventEmitter(); + const EventEmitter = CoreManager.getEventEmitter() as new () => EventEmitter; this.emitter = new EventEmitter(); this.on = this.emitter.on; this.emit = this.emitter.emit; // adding listener so process does not crash // best practice is for developer to register their own listener - this.on('error', () => {}); + this.on('error', () => { }); } shouldOpen(): any { @@ -184,9 +190,11 @@ class LiveQueryClient { * @param {string} sessionToken (optional) * @returns {LiveQuerySubscription | undefined} */ - subscribe(query: Object, sessionToken: ?string): LiveQuerySubscription { + subscribe(query: ParseQuery, sessionToken?: string): LiveQuerySubscription { if (!query) { - return; + // Might seem counterintuitive to simply return undefined instead of throwing + // But the tests currently expect this undefined return. + return undefined; } const className = query.className; const queryJSON = query.toJSON(); @@ -202,6 +210,7 @@ class LiveQueryClient { fields, watch, }, + sessionToken: undefined as string | undefined }; if (sessionToken) { @@ -228,7 +237,7 @@ class LiveQueryClient { * @param {object} subscription - subscription you would like to unsubscribe from. * @returns {Promise | undefined} */ - unsubscribe(subscription: Object): ?Promise { + async unsubscribe(subscription: LiveQuerySubscription): Promise { if (!subscription) { return; } @@ -272,7 +281,7 @@ class LiveQueryClient { }; this.socket.onclose = (event) => { - this.socket.closingPromise.resolve(event); + this.socket.closingPromise!.resolve(event); this._handleWebSocketClose(); }; @@ -297,6 +306,7 @@ class LiveQueryClient { where, fields, }, + sessionToken: undefined as string | undefined }; if (sessionToken) { @@ -315,7 +325,7 @@ class LiveQueryClient { * * @returns {Promise | undefined} CloseEvent {@link https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close_event} */ - close(): ?Promise { + async close(): Promise { if (this.state === CLIENT_STATE.INITIALIZED || this.state === CLIENT_STATE.DISCONNECTED) { return; } @@ -348,6 +358,7 @@ class LiveQueryClient { javascriptKey: this.javascriptKey, masterKey: this.masterKey, sessionToken: this.sessionToken, + installationId: undefined as string | undefined }; if (this.additionalProperties) { connectRequest.installationId = this.installationId; @@ -360,96 +371,96 @@ class LiveQueryClient { if (typeof data === 'string') { data = JSON.parse(data); } - let subscription = null; + let subscription: null | LiveQuerySubscription = null; if (data.requestId) { - subscription = this.subscriptions.get(data.requestId); + subscription = this.subscriptions.get(data.requestId) || null; } const response = { clientId: data.clientId, installationId: data.installationId, }; switch (data.op) { - case OP_EVENTS.CONNECTED: - if (this.state === CLIENT_STATE.RECONNECTING) { - this.resubscribe(); - } - this.emit(CLIENT_EMMITER_TYPES.OPEN); - this.id = data.clientId; - this.connectPromise.resolve(); - this.state = CLIENT_STATE.CONNECTED; - break; - case OP_EVENTS.SUBSCRIBED: - if (subscription) { - subscription.subscribed = true; - subscription.subscribePromise.resolve(); - setTimeout(() => subscription.emit(SUBSCRIPTION_EMMITER_TYPES.OPEN, response), 200); - } - break; - case OP_EVENTS.ERROR: { - const parseError = new ParseError(data.code, data.error); - if (!this.id) { - this.connectPromise.reject(parseError); - this.state = CLIENT_STATE.DISCONNECTED; - } - if (data.requestId) { + case OP_EVENTS.CONNECTED: + if (this.state === CLIENT_STATE.RECONNECTING) { + this.resubscribe(); + } + this.emit(CLIENT_EMMITER_TYPES.OPEN); + this.id = data.clientId; + this.connectPromise.resolve(); + this.state = CLIENT_STATE.CONNECTED; + break; + case OP_EVENTS.SUBSCRIBED: if (subscription) { - subscription.subscribePromise.reject(parseError); - setTimeout(() => subscription.emit(SUBSCRIPTION_EMMITER_TYPES.ERROR, data.error), 200); + subscription.subscribed = true; + subscription.subscribePromise.resolve(); + setTimeout(() => subscription!.emit(SUBSCRIPTION_EMMITER_TYPES.OPEN, response), 200); } - } else { - this.emit(CLIENT_EMMITER_TYPES.ERROR, data.error); - } - if (data.error === 'Additional properties not allowed') { - this.additionalProperties = false; - } - if (data.reconnect) { - this._handleReconnect(); - } - break; - } - case OP_EVENTS.UNSUBSCRIBED: { - if (subscription) { - this.subscriptions.delete(data.requestId); - subscription.subscribed = false; - subscription.unsubscribePromise.resolve(); - } - break; - } - default: { - // create, update, enter, leave, delete cases - if (!subscription) { break; - } - let override = false; - if (data.original) { - override = true; - delete data.original.__type; - // Check for removed fields - for (const field in data.original) { - if (!(field in data.object)) { - data.object[field] = undefined; + case OP_EVENTS.ERROR: { + const parseError = new ParseError(data.code, data.error); + if (!this.id) { + this.connectPromise.reject(parseError); + this.state = CLIENT_STATE.DISCONNECTED; + } + if (data.requestId) { + if (subscription) { + subscription.subscribePromise.reject(parseError); + setTimeout(() => subscription!.emit(SUBSCRIPTION_EMMITER_TYPES.ERROR, data.error), 200); } + } else { + this.emit(CLIENT_EMMITER_TYPES.ERROR, data.error); + } + if (data.error === 'Additional properties not allowed') { + this.additionalProperties = false; } - data.original = ParseObject.fromJSON(data.original, false); + if (data.reconnect) { + this._handleReconnect(); + } + break; } - delete data.object.__type; - const parseObject = ParseObject.fromJSON( - data.object, - !(subscription.query && subscription.query._select) ? override : false - ); - - if (data.original) { - subscription.emit(data.op, parseObject, data.original, response); - } else { - subscription.emit(data.op, parseObject, response); + case OP_EVENTS.UNSUBSCRIBED: { + if (subscription) { + this.subscriptions.delete(data.requestId); + subscription.subscribed = false; + subscription.unsubscribePromise.resolve(); + } + break; } + default: { + // create, update, enter, leave, delete cases + if (!subscription) { + break; + } + let override = false; + if (data.original) { + override = true; + delete data.original.__type; + // Check for removed fields + for (const field in data.original) { + if (!(field in data.object)) { + data.object[field] = undefined; + } + } + data.original = ParseObject.fromJSON(data.original, false); + } + delete data.object.__type; + const parseObject = ParseObject.fromJSON( + data.object, + !(subscription.query && subscription.query._select) ? override : false + ); + + if (data.original) { + subscription.emit(data.op, parseObject, data.original, response); + } else { + subscription.emit(data.op, parseObject, response); + } - const localDatastore = CoreManager.getLocalDatastore(); - if (override && localDatastore.isEnabled) { - localDatastore._updateObjectIfPinned(parseObject).then(() => {}); + const localDatastore = CoreManager.getLocalDatastore(); + if (override && localDatastore.isEnabled) { + localDatastore._updateObjectIfPinned(parseObject).then(() => { }); + } } } - } } _handleWebSocketClose() { @@ -506,12 +517,11 @@ if (process.env.PARSE_BUILD === 'node') { CoreManager.setWebSocketController(require('ws')); } else if (process.env.PARSE_BUILD === 'browser') { CoreManager.setWebSocketController( - typeof WebSocket === 'function' || typeof WebSocket === 'object' ? WebSocket : null + typeof WebSocket === 'function' || typeof WebSocket === 'object' ? WebSocket : null as any ); } else if (process.env.PARSE_BUILD === 'weapp') { CoreManager.setWebSocketController(require('./Socket.weapp')); } else if (process.env.PARSE_BUILD === 'react-native') { - CoreManager.setWebSocketController(WebSocket); + CoreManager.setWebSocketController(WebSocket as any); } - export default LiveQueryClient; diff --git a/src/LiveQuerySubscription.js b/src/LiveQuerySubscription.ts similarity index 85% rename from src/LiveQuerySubscription.js rename to src/LiveQuerySubscription.ts index c70d234c1..62ff1de4f 100644 --- a/src/LiveQuerySubscription.js +++ b/src/LiveQuerySubscription.ts @@ -1,5 +1,8 @@ +import type { EventEmitter } from 'events'; import CoreManager from './CoreManager'; +import AnEventEmitter from './EventEmitter'; import { resolvingPromise } from './promiseUtils'; +import type ParseQuery from './ParseQuery'; /** * Creates a new LiveQuery Subscription. @@ -84,26 +87,36 @@ import { resolvingPromise } from './promiseUtils'; * });

*/ class Subscription { - /* + id: string | number; + query: ParseQuery; + sessionToken?: string; + subscribePromise: ReturnType>; + unsubscribePromise: ReturnType>; + subscribed: boolean; + emitter: EventEmitter; + on: EventEmitter['on']; + emit: EventEmitter['emit']; + + /** * @param {string} id - subscription id * @param {string} query - query to subscribe to * @param {string} sessionToken - optional session token */ - constructor(id, query, sessionToken) { + constructor(id: string | number, query: ParseQuery, sessionToken?: string) { this.id = id; this.query = query; this.sessionToken = sessionToken; this.subscribePromise = resolvingPromise(); this.unsubscribePromise = resolvingPromise(); this.subscribed = false; - const EventEmitter = CoreManager.getEventEmitter(); + const EventEmitter = CoreManager.getEventEmitter() as new () => EventEmitter; this.emitter = new EventEmitter(); this.on = this.emitter.on; this.emit = this.emitter.emit; // adding listener so process does not crash // best practice is for developer to register their own listener - this.on('error', () => {}); + this.on('error', () => { }); } /** @@ -111,7 +124,7 @@ class Subscription { * * @returns {Promise} */ - unsubscribe(): Promise { + unsubscribe(): Promise { return CoreManager.getLiveQueryController() .getDefaultLiveQueryClient() .then(liveQueryClient => { diff --git a/src/LocalDatastore.js b/src/LocalDatastore.ts similarity index 98% rename from src/LocalDatastore.js rename to src/LocalDatastore.ts index bf552d09d..213b9d0c0 100644 --- a/src/LocalDatastore.js +++ b/src/LocalDatastore.ts @@ -37,12 +37,12 @@ const LocalDatastore = { return controller.fromPinWithName(name); }, - pinWithName(name: string, value: any): Promise { + async pinWithName(name: string, value: any): Promise { const controller = CoreManager.getLocalDatastoreController(); return controller.pinWithName(name, value); }, - unPinWithName(name: string): Promise { + async unPinWithName(name: string): Promise { const controller = CoreManager.getLocalDatastoreController(); return controller.unPinWithName(name); }, @@ -55,10 +55,10 @@ const LocalDatastore = { // Use for testing _getRawStorage(): Promise { const controller = CoreManager.getLocalDatastoreController(); - return controller.getRawStorage(); + return (controller as any).getRawStorage(); }, - _clear(): Promise { + async _clear(): Promise { const controller = CoreManager.getLocalDatastoreController(); return controller.clear(); }, @@ -376,7 +376,7 @@ const LocalDatastore = { return `${OBJECT_PREFIX}${object.className}_${objectId}`; }, - getPinName(pinName: ?string) { + getPinName(pinName?: string) { if (!pinName || pinName === DEFAULT_PIN) { return DEFAULT_PIN; } diff --git a/src/LocalDatastoreController.react-native.js b/src/LocalDatastoreController.react-native.ts similarity index 98% rename from src/LocalDatastoreController.react-native.js rename to src/LocalDatastoreController.react-native.ts index 74ebde95f..1fede2801 100644 --- a/src/LocalDatastoreController.react-native.js +++ b/src/LocalDatastoreController.react-native.ts @@ -86,3 +86,4 @@ const LocalDatastoreController = { }; module.exports = LocalDatastoreController; +export default LocalDatastoreController; diff --git a/src/LocalDatastoreController.js b/src/LocalDatastoreController.ts similarity index 87% rename from src/LocalDatastoreController.js rename to src/LocalDatastoreController.ts index 7116f28fd..f2d050270 100644 --- a/src/LocalDatastoreController.js +++ b/src/LocalDatastoreController.ts @@ -5,7 +5,7 @@ import { isLocalDatastoreKey } from './LocalDatastoreUtils'; import Storage from './Storage'; const LocalDatastoreController = { - async fromPinWithName(name: string): Array { + async fromPinWithName(name: string): Promise> { const values = await Storage.getItemAsync(name); if (!values) { return []; @@ -23,7 +23,7 @@ const LocalDatastoreController = { return Storage.removeItemAsync(name); }, - async getAllContents(): Object { + async getAllContents(): Promise { const keys = await Storage.getAllKeysAsync(); return keys.reduce(async (previousPromise, key) => { const LDS = await previousPromise; @@ -40,7 +40,7 @@ const LocalDatastoreController = { }, // Used for testing - async getRawStorage(): Object { + async getRawStorage(): Promise { const keys = await Storage.getAllKeysAsync(); return keys.reduce(async (previousPromise, key) => { const LDS = await previousPromise; @@ -50,7 +50,7 @@ const LocalDatastoreController = { }, Promise.resolve({})); }, - async clear(): Promise { + async clear(): Promise { const keys = await Storage.getAllKeysAsync(); const toRemove = []; @@ -65,3 +65,4 @@ const LocalDatastoreController = { }; module.exports = LocalDatastoreController; +export default LocalDatastoreController; diff --git a/src/LocalDatastoreUtils.js b/src/LocalDatastoreUtils.ts similarity index 100% rename from src/LocalDatastoreUtils.js rename to src/LocalDatastoreUtils.ts diff --git a/src/ObjectStateMutations.js b/src/ObjectStateMutations.ts similarity index 89% rename from src/ObjectStateMutations.js rename to src/ObjectStateMutations.ts index d594d5d70..219de91b8 100644 --- a/src/ObjectStateMutations.js +++ b/src/ObjectStateMutations.ts @@ -43,7 +43,7 @@ export function setServerData(serverData: AttributeMap, attributes: AttributeMap } } -export function setPendingOp(pendingOps: Array, attr: string, op: ?Op) { +export function setPendingOp(pendingOps: Array, attr: string, op?: Op) { const last = pendingOps.length - 1; if (op) { pendingOps[last][attr] = op; @@ -56,7 +56,7 @@ export function pushPendingState(pendingOps: Array) { pendingOps.push({}); } -export function popPendingState(pendingOps: Array): OpsMap { +export function popPendingState(pendingOps: Array): OpsMap | undefined { const first = pendingOps.shift(); if (!pendingOps.length) { pendingOps[0] = {}; @@ -83,9 +83,9 @@ export function estimateAttribute( serverData: AttributeMap, pendingOps: Array, className: string, - id: ?string, + id: string | undefined, attr: string -): mixed { +): any { let value = serverData[attr]; for (let i = 0; i < pendingOps.length; i++) { if (pendingOps[i][attr]) { @@ -105,7 +105,7 @@ export function estimateAttributes( serverData: AttributeMap, pendingOps: Array, className: string, - id: ?string + id?: string ): AttributeMap { const data = {}; let attr; @@ -147,7 +147,14 @@ export function estimateAttributes( return data; } -function nestedSet(obj, key, value) { +/** + * Allows setting properties/variables deep in an object. + * @param obj The object to assign the value to + * @param key The key to assign. If it's in a deeper path, then use dot notation (`prop1.prop2.prop3`) + * Note that intermediate object(s) in the nested path are automatically created if they don't exist. + * @param value The value to assign. If it's an `undefined` then the key is deleted. + */ +function nestedSet(obj: AttributeMap, key: string, value: any) { const path = key.split('.'); for (let i = 0; i < path.length - 1; i++) { if (!(path[i] in obj)) { diff --git a/src/OfflineQuery.js b/src/OfflineQuery.ts similarity index 52% rename from src/OfflineQuery.js rename to src/OfflineQuery.ts index efbc200c9..e0477e16f 100644 --- a/src/OfflineQuery.js +++ b/src/OfflineQuery.ts @@ -1,8 +1,13 @@ -const equalObjects = require('./equals').default; -const decode = require('./decode').default; -const ParseError = require('./ParseError').default; -const ParsePolygon = require('./ParsePolygon').default; -const ParseGeoPoint = require('./ParseGeoPoint').default; +import { AttributeMap } from "./ObjectStateMutations"; +import type ParseObject from "./ParseObject"; +import type Query from "./ParseQuery"; +import type { QueryJSON, WhereClause } from './ParseQuery'; + +import equalObjects from './equals'; +import decode from './decode'; +import ParseError from './ParseError'; +import ParsePolygon from './ParsePolygon'; +import ParseGeoPoint from './ParseGeoPoint'; /** * contains -- Determines if an object is contained in a list with special handling for Parse pointers. * @@ -11,7 +16,7 @@ const ParseGeoPoint = require('./ParseGeoPoint').default; * @private * @returns {boolean} */ -function contains(haystack, needle) { +function contains(haystack: any[], needle: any) { if (needle && needle.__type && (needle.__type === 'Pointer' || needle.__type === 'Object')) { for (const i in haystack) { const ptr = haystack[i]; @@ -54,11 +59,11 @@ function transformObject(object) { * @private * @returns {boolean} */ -function matchesQuery(className, object, objects, query) { +function matchesQuery(className: string, object: ParseObject, objects: ParseObject[], query: WhereClause) { if (object.className !== className) { return false; } - let obj = object; + let obj: AttributeMap | ParseObject = object; let q = query; if (object.toJSON) { obj = object.toJSON(); @@ -160,52 +165,52 @@ function relativeTimeToDate(text, now = new Date()) { } switch (interval) { - case 'yr': - case 'yrs': - case 'year': - case 'years': - seconds += val * 31536000; // 365 * 24 * 60 * 60 - break; - - case 'wk': - case 'wks': - case 'week': - case 'weeks': - seconds += val * 604800; // 7 * 24 * 60 * 60 - break; - - case 'd': - case 'day': - case 'days': - seconds += val * 86400; // 24 * 60 * 60 - break; - - case 'hr': - case 'hrs': - case 'hour': - case 'hours': - seconds += val * 3600; // 60 * 60 - break; - - case 'min': - case 'mins': - case 'minute': - case 'minutes': - seconds += val * 60; - break; - - case 'sec': - case 'secs': - case 'second': - case 'seconds': - seconds += val; - break; - - default: - return { - status: 'error', - info: `Invalid interval: '${interval}'`, - }; + case 'yr': + case 'yrs': + case 'year': + case 'years': + seconds += val * 31536000; // 365 * 24 * 60 * 60 + break; + + case 'wk': + case 'wks': + case 'week': + case 'weeks': + seconds += val * 604800; // 7 * 24 * 60 * 60 + break; + + case 'd': + case 'day': + case 'days': + seconds += val * 86400; // 24 * 60 * 60 + break; + + case 'hr': + case 'hrs': + case 'hour': + case 'hours': + seconds += val * 3600; // 60 * 60 + break; + + case 'min': + case 'mins': + case 'minute': + case 'minutes': + seconds += val * 60; + break; + + case 'sec': + case 'secs': + case 'second': + case 'seconds': + seconds += val; + break; + + default: + return { + status: 'error', + info: `Invalid interval: '${interval}'`, + }; } } @@ -333,229 +338,229 @@ function matchesKeyConstraints(className, object, objects, key, constraints) { if ( toString.call(compareTo) === '[object Date]' || (typeof compareTo === 'string' && - new Date(compareTo) !== 'Invalid Date' && - !isNaN(new Date(compareTo))) + new Date(compareTo).toString() !== 'Invalid Date' && + !isNaN(new Date(compareTo).getTime())) ) { object[key] = new Date(object[key].iso ? object[key].iso : object[key]); } switch (condition) { - case '$lt': - if (object[key] >= compareTo) { - return false; - } - break; - case '$lte': - if (object[key] > compareTo) { - return false; - } - break; - case '$gt': - if (object[key] <= compareTo) { - return false; - } - break; - case '$gte': - if (object[key] < compareTo) { - return false; - } - break; - case '$ne': - if (equalObjects(object[key], compareTo)) { - return false; - } - break; - case '$in': - if (!contains(compareTo, object[key])) { - return false; - } - break; - case '$nin': - if (contains(compareTo, object[key])) { - return false; - } - break; - case '$all': - for (i = 0; i < compareTo.length; i++) { - if (object[key].indexOf(compareTo[i]) < 0) { + case '$lt': + if (object[key] >= compareTo) { return false; } - } - break; - case '$exists': { - const propertyExists = typeof object[key] !== 'undefined'; - const existenceIsRequired = constraints['$exists']; - if (typeof constraints['$exists'] !== 'boolean') { - // The SDK will never submit a non-boolean for $exists, but if someone - // tries to submit a non-boolean for $exits outside the SDKs, just ignore it. break; - } - if ((!propertyExists && existenceIsRequired) || (propertyExists && !existenceIsRequired)) { - return false; - } - break; - } - case '$regex': { - if (typeof compareTo === 'object') { - return compareTo.test(object[key]); - } - // JS doesn't support perl-style escaping - let expString = ''; - let escapeEnd = -2; - let escapeStart = compareTo.indexOf('\\Q'); - while (escapeStart > -1) { - // Add the unescaped portion - expString += compareTo.substring(escapeEnd + 2, escapeStart); - escapeEnd = compareTo.indexOf('\\E', escapeStart); - if (escapeEnd > -1) { - expString += compareTo - .substring(escapeStart + 2, escapeEnd) - .replace(/\\\\\\\\E/g, '\\E') - .replace(/\W/g, '\\$&'); + case '$lte': + if (object[key] > compareTo) { + return false; } - - escapeStart = compareTo.indexOf('\\Q', escapeEnd); - } - expString += compareTo.substring(Math.max(escapeStart, escapeEnd + 2)); - let modifiers = constraints.$options || ''; - modifiers = modifiers.replace('x', '').replace('s', ''); - // Parse Server / Mongo support x and s modifiers but JS RegExp doesn't - const exp = new RegExp(expString, modifiers); - if (!exp.test(object[key])) { - return false; - } - break; - } - case '$nearSphere': { - if (!compareTo || !object[key]) { - return false; + break; + case '$gt': + if (object[key] <= compareTo) { + return false; + } + break; + case '$gte': + if (object[key] < compareTo) { + return false; + } + break; + case '$ne': + if (equalObjects(object[key], compareTo)) { + return false; + } + break; + case '$in': + if (!contains(compareTo, object[key])) { + return false; + } + break; + case '$nin': + if (contains(compareTo, object[key])) { + return false; + } + break; + case '$all': + for (i = 0; i < compareTo.length; i++) { + if (object[key].indexOf(compareTo[i]) < 0) { + return false; + } + } + break; + case '$exists': { + const propertyExists = typeof object[key] !== 'undefined'; + const existenceIsRequired = constraints['$exists']; + if (typeof constraints['$exists'] !== 'boolean') { + // The SDK will never submit a non-boolean for $exists, but if someone + // tries to submit a non-boolean for $exits outside the SDKs, just ignore it. + break; + } + if ((!propertyExists && existenceIsRequired) || (propertyExists && !existenceIsRequired)) { + return false; + } + break; } - const distance = compareTo.radiansTo(object[key]); - const max = constraints.$maxDistance || Infinity; - return distance <= max; - } - case '$within': { - if (!compareTo || !object[key]) { - return false; + case '$regex': { + if (typeof compareTo === 'object') { + return compareTo.test(object[key]); + } + // JS doesn't support perl-style escaping + let expString = ''; + let escapeEnd = -2; + let escapeStart = compareTo.indexOf('\\Q'); + while (escapeStart > -1) { + // Add the unescaped portion + expString += compareTo.substring(escapeEnd + 2, escapeStart); + escapeEnd = compareTo.indexOf('\\E', escapeStart); + if (escapeEnd > -1) { + expString += compareTo + .substring(escapeStart + 2, escapeEnd) + .replace(/\\\\\\\\E/g, '\\E') + .replace(/\W/g, '\\$&'); + } + + escapeStart = compareTo.indexOf('\\Q', escapeEnd); + } + expString += compareTo.substring(Math.max(escapeStart, escapeEnd + 2)); + let modifiers = constraints.$options || ''; + modifiers = modifiers.replace('x', '').replace('s', ''); + // Parse Server / Mongo support x and s modifiers but JS RegExp doesn't + const exp = new RegExp(expString, modifiers); + if (!exp.test(object[key])) { + return false; + } + break; } - const southWest = compareTo.$box[0]; - const northEast = compareTo.$box[1]; - if (southWest.latitude > northEast.latitude || southWest.longitude > northEast.longitude) { - // Invalid box, crosses the date line - return false; + case '$nearSphere': { + if (!compareTo || !object[key]) { + return false; + } + const distance = compareTo.radiansTo(object[key]); + const max = constraints.$maxDistance || Infinity; + return distance <= max; } - return ( - object[key].latitude > southWest.latitude && + case '$within': { + if (!compareTo || !object[key]) { + return false; + } + const southWest = compareTo.$box[0]; + const northEast = compareTo.$box[1]; + if (southWest.latitude > northEast.latitude || southWest.longitude > northEast.longitude) { + // Invalid box, crosses the date line + return false; + } + return ( + object[key].latitude > southWest.latitude && object[key].latitude < northEast.latitude && object[key].longitude > southWest.longitude && object[key].longitude < northEast.longitude - ); - } - case '$options': - // Not a query type, but a way to add options to $regex. Ignore and - // avoid the default - break; - case '$maxDistance': - // Not a query type, but a way to add a cap to $nearSphere. Ignore and - // avoid the default - break; - case '$select': { - const subQueryObjects = objects.filter((obj, index, arr) => { - return matchesQuery(compareTo.query.className, obj, arr, compareTo.query.where); - }); - for (let i = 0; i < subQueryObjects.length; i += 1) { - const subObject = transformObject(subQueryObjects[i]); - return equalObjects(object[key], subObject[compareTo.key]); + ); } - return false; - } - case '$dontSelect': { - const subQueryObjects = objects.filter((obj, index, arr) => { - return matchesQuery(compareTo.query.className, obj, arr, compareTo.query.where); - }); - for (let i = 0; i < subQueryObjects.length; i += 1) { - const subObject = transformObject(subQueryObjects[i]); - return !equalObjects(object[key], subObject[compareTo.key]); + case '$options': + // Not a query type, but a way to add options to $regex. Ignore and + // avoid the default + break; + case '$maxDistance': + // Not a query type, but a way to add a cap to $nearSphere. Ignore and + // avoid the default + break; + case '$select': { + const subQueryObjects = objects.filter((obj, index, arr) => { + return matchesQuery(compareTo.query.className, obj, arr, compareTo.query.where); + }); + for (let i = 0; i < subQueryObjects.length; i += 1) { + const subObject = transformObject(subQueryObjects[i]); + return equalObjects(object[key], subObject[compareTo.key]); + } + return false; } - return false; - } - case '$inQuery': { - const subQueryObjects = objects.filter((obj, index, arr) => { - return matchesQuery(compareTo.className, obj, arr, compareTo.where); - }); + case '$dontSelect': { + const subQueryObjects = objects.filter((obj, index, arr) => { + return matchesQuery(compareTo.query.className, obj, arr, compareTo.query.where); + }); + for (let i = 0; i < subQueryObjects.length; i += 1) { + const subObject = transformObject(subQueryObjects[i]); + return !equalObjects(object[key], subObject[compareTo.key]); + } + return false; + } + case '$inQuery': { + const subQueryObjects = objects.filter((obj, index, arr) => { + return matchesQuery(compareTo.className, obj, arr, compareTo.where); + }); - for (let i = 0; i < subQueryObjects.length; i += 1) { - const subObject = transformObject(subQueryObjects[i]); - if ( - object[key].className === subObject.className && + for (let i = 0; i < subQueryObjects.length; i += 1) { + const subObject = transformObject(subQueryObjects[i]); + if ( + object[key].className === subObject.className && object[key].objectId === subObject.objectId - ) { - return true; + ) { + return true; + } } + return false; } - return false; - } - case '$notInQuery': { - const subQueryObjects = objects.filter((obj, index, arr) => { - return matchesQuery(compareTo.className, obj, arr, compareTo.where); - }); + case '$notInQuery': { + const subQueryObjects = objects.filter((obj, index, arr) => { + return matchesQuery(compareTo.className, obj, arr, compareTo.where); + }); - for (let i = 0; i < subQueryObjects.length; i += 1) { - const subObject = transformObject(subQueryObjects[i]); - if ( - object[key].className === subObject.className && + for (let i = 0; i < subQueryObjects.length; i += 1) { + const subObject = transformObject(subQueryObjects[i]); + if ( + object[key].className === subObject.className && object[key].objectId === subObject.objectId - ) { - return false; + ) { + return false; + } } + return true; } - return true; - } - case '$containedBy': { - for (const value of object[key]) { - if (!contains(compareTo, value)) { - return false; + case '$containedBy': { + for (const value of object[key]) { + if (!contains(compareTo, value)) { + return false; + } } + return true; } - return true; - } - case '$geoWithin': { - if (compareTo.$polygon) { - const points = compareTo.$polygon.map(geoPoint => [ - geoPoint.latitude, - geoPoint.longitude, - ]); - const polygon = new ParsePolygon(points); - return polygon.containsPoint(object[key]); + case '$geoWithin': { + if (compareTo.$polygon) { + const points = compareTo.$polygon.map(geoPoint => [ + geoPoint.latitude, + geoPoint.longitude, + ]); + const polygon = new ParsePolygon(points); + return polygon.containsPoint(object[key]); + } + if (compareTo.$centerSphere) { + const [WGS84Point, maxDistance] = compareTo.$centerSphere; + const centerPoint = new ParseGeoPoint({ + latitude: WGS84Point[1], + longitude: WGS84Point[0], + }); + const point = new ParseGeoPoint(object[key]); + const distance = point.radiansTo(centerPoint); + return distance <= maxDistance; + } + return false; } - if (compareTo.$centerSphere) { - const [WGS84Point, maxDistance] = compareTo.$centerSphere; - const centerPoint = new ParseGeoPoint({ - latitude: WGS84Point[1], - longitude: WGS84Point[0], - }); - const point = new ParseGeoPoint(object[key]); - const distance = point.radiansTo(centerPoint); - return distance <= maxDistance; + case '$geoIntersects': { + const polygon = new ParsePolygon(object[key].coordinates); + const point = new ParseGeoPoint(compareTo.$point); + return polygon.containsPoint(point); } - return false; - } - case '$geoIntersects': { - const polygon = new ParsePolygon(object[key].coordinates); - const point = new ParseGeoPoint(compareTo.$point); - return polygon.containsPoint(point); - } - default: - return false; + default: + return false; } } return true; } -function validateQuery(query: any) { - let q = query; +function validateQuery(query: Query | QueryJSON) { + let q: Query | QueryJSON | WhereClause = query; - if (query.toJSON) { + if ('toJSON' in query) { q = query.toJSON().where; } const specialQuerykeys = [ @@ -594,3 +599,4 @@ const OfflineQuery = { }; module.exports = OfflineQuery; +export default OfflineQuery; diff --git a/src/Parse.ts b/src/Parse.ts index dbf3a0983..0085383ff 100644 --- a/src/Parse.ts +++ b/src/Parse.ts @@ -1,10 +1,7 @@ -import decode from './decode'; -import encode from './encode'; import CryptoController from './CryptoController'; import EventuallyQueue from './EventuallyQueue'; import IndexedDBStorageController from './IndexedDBStorageController'; import InstallationController from './InstallationController'; -import * as ParseOp from './ParseOp'; import RESTController from './RESTController'; import ACL from './ParseACL'; import * as Analytics from './Analytics' @@ -21,7 +18,7 @@ import GeoPoint from './ParseGeoPoint' import Polygon from './ParsePolygon' import Installation from './ParseInstallation' import LocalDatastore from './LocalDatastore' -import Object from './ParseObject' +import ParseObject from './ParseObject' import * as Push from './Push' import Query from './ParseQuery' import Relation from './ParseRelation' @@ -32,6 +29,10 @@ import Storage from './Storage' import User from './ParseUser' import LiveQuery from './ParseLiveQuery' import LiveQueryClient from './LiveQueryClient' +// Need to reorder these last due to them requiring ParseObject/ParseRole, etc to be defined first +import * as ParseOp from './ParseOp'; +import decode from './decode'; +import encode from './encode'; /** * Contains all Parse API classes and functions. @@ -47,7 +48,10 @@ interface ParseType { Parse?: ParseType, Analytics: typeof Analytics, AnonymousUtils: typeof AnonymousUtils, - Cloud: typeof Cloud, + Cloud: typeof Cloud & { + /** only availabe in server environments */ + useMasterKey?: () => void + }, CLP: typeof CLP, CoreManager: typeof CoreManager, Config: typeof Config, @@ -60,7 +64,7 @@ interface ParseType { Polygon: typeof Polygon, Installation: typeof Installation, LocalDatastore: typeof LocalDatastore, - Object: typeof Object, + Object: typeof ParseObject, Op: { Set: typeof ParseOp.SetOp, Unset: typeof ParseOp.UnsetOp, @@ -78,7 +82,7 @@ interface ParseType { Session: typeof Session, Storage: typeof Storage, User: typeof User, - LiveQuery?: typeof LiveQuery, + LiveQuery?: LiveQuery, LiveQueryClient: typeof LiveQueryClient, initialize(applicationId: string, javaScriptKey: string): void, @@ -103,7 +107,7 @@ interface ParseType { _ajax(...args: any[]): void, _decode(...args: any[]): void, _encode(...args: any[]): void, - _getInstallationId?(): string, + _getInstallationId?(): Promise, enableLocalDatastore(polling: boolean, ms: number): void, isLocalDatastoreEnabled(): boolean, dumpLocalDatastore(): void, @@ -114,39 +118,39 @@ interface ParseType { const Parse: ParseType = { ACL: ACL, Analytics: Analytics, - AnonymousUtils: AnonymousUtils, + AnonymousUtils: AnonymousUtils, Cloud: Cloud, CLP: CLP, - CoreManager: CoreManager, - Config: Config, - Error: ParseError, - EventuallyQueue: EventuallyQueue, + CoreManager: CoreManager, + Config: Config, + Error: ParseError, + EventuallyQueue: EventuallyQueue, FacebookUtils: FacebookUtils, - File: File, - GeoPoint: GeoPoint, - Polygon: Polygon, - Installation: Installation, - LocalDatastore: LocalDatastore, - Object: Object, + File: File, + GeoPoint: GeoPoint, + Polygon: Polygon, + Installation: Installation, + LocalDatastore: LocalDatastore, + Object: ParseObject, Op: { - Set: ParseOp.SetOp, - Unset: ParseOp.UnsetOp, - Increment: ParseOp.IncrementOp, - Add: ParseOp.AddOp, - Remove: ParseOp.RemoveOp, - AddUnique: ParseOp.AddUniqueOp, - Relation: ParseOp.RelationOp, - }, - Push: Push, - Query: Query, - Relation: Relation, - Role: Role, - Schema: Schema, - Session: Session, - Storage: Storage, - User: User, - LiveQueryClient: LiveQueryClient, - LiveQuery: undefined, + Set: ParseOp.SetOp, + Unset: ParseOp.UnsetOp, + Increment: ParseOp.IncrementOp, + Add: ParseOp.AddOp, + Remove: ParseOp.RemoveOp, + AddUnique: ParseOp.AddUniqueOp, + Relation: ParseOp.RelationOp, + }, + Push: Push, + Query: Query, + Relation: Relation, + Role: Role, + Schema: Schema, + Session: Session, + Storage: Storage, + User: User, + LiveQueryClient: LiveQueryClient, + LiveQuery: undefined, IndexedDB: undefined, Hooks: undefined, Parse: undefined, @@ -168,7 +172,7 @@ const Parse: ParseType = { /* eslint-disable no-console */ console.log( "It looks like you're using the browser version of the SDK in a " + - "node.js environment. You should require('parse/node') instead." + "node.js environment. You should require('parse/node') instead." ); /* eslint-enable no-console */ } @@ -361,7 +365,7 @@ const Parse: ParseType = { return encode(value, disallowObjects); }, - _getInstallationId () { + _getInstallationId() { return CoreManager.getInstallationController().currentInstallationId(); }, /** @@ -390,7 +394,7 @@ const Parse: ParseType = { * @static * @returns {boolean} */ - isLocalDatastoreEnabled () { + isLocalDatastoreEnabled() { return this.LocalDatastore.isEnabled; }, /** @@ -418,7 +422,7 @@ const Parse: ParseType = { * * @static */ - enableEncryptedUser () { + enableEncryptedUser() { this.encryptedUser = true; }, @@ -428,7 +432,7 @@ const Parse: ParseType = { * @static * @returns {boolean} */ - isEncryptedUserEnabled () { + isEncryptedUserEnabled() { return this.encryptedUser; }, }; @@ -439,7 +443,7 @@ CoreManager.setRESTController(RESTController); if (process.env.PARSE_BUILD === 'node') { Parse.initialize = Parse._initialize; - Parse.Cloud = Parse.Cloud || {}; + Parse.Cloud = Parse.Cloud || {} as any; Parse.Cloud.useMasterKey = function () { CoreManager.set('USE_MASTER_KEY', true); }; diff --git a/src/ParseACL.js b/src/ParseACL.ts similarity index 92% rename from src/ParseACL.js rename to src/ParseACL.ts index beedb6719..c1b0bbad5 100644 --- a/src/ParseACL.js +++ b/src/ParseACL.ts @@ -101,7 +101,8 @@ class ParseACL { _setAccess(accessType: string, userId: ParseUser | ParseRole | string, allowed: boolean) { if (userId instanceof ParseUser) { - userId = userId.id; + // We would expect the ParseUser to have an ID; we don't have Users without IDs, right? + userId = userId.id!; } else if (userId instanceof ParseRole) { const name = userId.getName(); if (!name) { @@ -138,7 +139,8 @@ class ParseACL { _getAccess(accessType: string, userId: ParseUser | ParseRole | string): boolean { if (userId instanceof ParseUser) { - userId = userId.id; + // We would expect the ParseUser to have an ID; we don't have Users without IDs, right? + userId = userId.id!; if (!userId) { throw new Error('Cannot get access for a ParseUser without an ID'); } @@ -250,7 +252,9 @@ class ParseACL { getRoleReadAccess(role: ParseRole | string): boolean { if (role instanceof ParseRole) { // Normalize to the String name - role = role.getName(); + // A forced cast here is likely harmless since we force check + // `typeof role === 'string'` after this. + role = role.getName()!; } if (typeof role !== 'string') { throw new TypeError('role must be a ParseRole or a String'); @@ -270,7 +274,9 @@ class ParseACL { getRoleWriteAccess(role: ParseRole | string): boolean { if (role instanceof ParseRole) { // Normalize to the String name - role = role.getName(); + // A forced cast here is likely harmless since we force check + // `typeof role === 'string'` after this. + role = role.getName()!; } if (typeof role !== 'string') { throw new TypeError('role must be a ParseRole or a String'); @@ -289,7 +295,9 @@ class ParseACL { setRoleReadAccess(role: ParseRole | string, allowed: boolean) { if (role instanceof ParseRole) { // Normalize to the String name - role = role.getName(); + // A forced cast here is likely harmless since we force check + // `typeof role === 'string'` after this. + role = role.getName()!; } if (typeof role !== 'string') { throw new TypeError('role must be a ParseRole or a String'); @@ -308,7 +316,9 @@ class ParseACL { setRoleWriteAccess(role: ParseRole | string, allowed: boolean) { if (role instanceof ParseRole) { // Normalize to the String name - role = role.getName(); + // A forced cast here is likely harmless since we force check + // `typeof role === 'string'` after this. + role = role.getName()!; } if (typeof role !== 'string') { throw new TypeError('role must be a ParseRole or a String'); diff --git a/src/ParseCLP.js b/src/ParseCLP.ts similarity index 94% rename from src/ParseCLP.js rename to src/ParseCLP.ts index ff86e387a..6be55d917 100644 --- a/src/ParseCLP.js +++ b/src/ParseCLP.ts @@ -5,12 +5,14 @@ import ParseRole from './ParseRole'; import ParseUser from './ParseUser'; -type Entity = Entity; +type Entity = ParseUser | ParseRole | string; type UsersMap = { [userId: string]: boolean | any }; -export type PermissionsMap = { [permission: string]: UsersMap }; +export type PermissionsMap = { writeUserFields?: string[], readUserFields?: string[] } & { [permission: string]: UsersMap }; const PUBLIC_KEY = '*'; +// TODO: Typescript; This seems like a convenient way to define things quick & easily.. +// But it is quite type-unsafe right now. Seems to only be used in this file too. const VALID_PERMISSIONS: Map = new Map(); VALID_PERMISSIONS.set('get', {}); VALID_PERMISSIONS.set('find', {}); @@ -263,7 +265,7 @@ class ParseCLP { let name = role; if (role instanceof ParseRole) { // Normalize to the String name - name = role.getName(); + name = role.getName()!; } if (typeof name !== 'string') { throw new TypeError('role must be a Parse.Role or a String'); @@ -274,7 +276,7 @@ class ParseCLP { _parseEntity(entity: Entity) { let userId = entity; if (userId instanceof ParseUser) { - userId = userId.id; + userId = userId.id!; if (!userId) { throw new Error('Cannot get access for a Parse.User without an id.'); } @@ -323,7 +325,7 @@ class ParseCLP { return permissions; } - _setArrayAccess(permission: string, userId: Entity, fields: string) { + _setArrayAccess(permission: string, userId: Entity, fields: string | string[]) { userId = this._parseEntity(userId); const permissions = this.permissionsMap[permission][userId]; @@ -356,7 +358,7 @@ class ParseCLP { } } - _getGroupPointerPermissions(operation: string): string[] { + _getGroupPointerPermissions(operation: 'readUserFields' | 'writeUserFields'): string[] | undefined { return this.permissionsMap[operation]; } @@ -373,7 +375,7 @@ class ParseCLP { * @returns {string[]} User pointer fields */ getReadUserFields(): string[] { - return this._getGroupPointerPermissions('readUserFields'); + return this._getGroupPointerPermissions('readUserFields') || []; } /** @@ -389,7 +391,7 @@ class ParseCLP { * @returns {string[]} User pointer fields */ getWriteUserFields(): string[] { - return this._getGroupPointerPermissions('writeUserFields'); + return this._getGroupPointerPermissions('writeUserFields') || []; } /** @@ -409,7 +411,7 @@ class ParseCLP { * @returns {string[]} */ getProtectedFields(userId: Entity): string[] { - return this._getAccess('protectedFields', userId, false); + return this._getAccess('protectedFields', userId, false) as string[]; } /** @@ -435,9 +437,9 @@ class ParseCLP { */ getReadAccess(userId: Entity): boolean { return ( - this._getAccess('find', userId) && - this._getAccess('get', userId) && - this._getAccess('count', userId) + this._getAccess('find', userId) as boolean && + this._getAccess('get', userId) as boolean && + this._getAccess('count', userId) as boolean ); } @@ -465,10 +467,10 @@ class ParseCLP { */ getWriteAccess(userId: Entity): boolean { return ( - this._getAccess('create', userId) && - this._getAccess('update', userId) && - this._getAccess('delete', userId) && - this._getAccess('addField', userId) + this._getAccess('create', userId) as boolean && + this._getAccess('update', userId) as boolean && + this._getAccess('delete', userId) as boolean && + this._getAccess('addField', userId) as boolean ); } diff --git a/src/ParseConfig.js b/src/ParseConfig.ts similarity index 97% rename from src/ParseConfig.js rename to src/ParseConfig.ts index 57620deb6..9f18dc7d4 100644 --- a/src/ParseConfig.js +++ b/src/ParseConfig.ts @@ -18,7 +18,7 @@ import type { RequestOptions } from './RESTController'; * @alias Parse.Config */ -class ParseConfig { +export class ParseConfig { attributes: { [key: string]: any }; _escapedAttributes: { [key: string]: any }; @@ -123,7 +123,7 @@ class ParseConfig { } } -let currentConfig = null; +let currentConfig: ParseConfig | null = null; const CURRENT_CONFIG_KEY = 'currentConfig'; @@ -139,7 +139,7 @@ function decodePayload(data) { } const DefaultController = { - current() { + current: ()=> { if (currentConfig) { return currentConfig; } @@ -195,7 +195,7 @@ const DefaultController = { }); }, - save(attrs: { [key: string]: any }, masterKeyOnlyFlags: { [key: string]: any }) { + save(attrs?: { [key: string]: any }, masterKeyOnlyFlags?: { [key: string]: any }) { const RESTController = CoreManager.getRESTController(); const encodedAttrs = {}; for (const key in attrs) { diff --git a/src/ParseError.js b/src/ParseError.js deleted file mode 100644 index 68c97501e..000000000 --- a/src/ParseError.js +++ /dev/null @@ -1,551 +0,0 @@ -import CoreManager from './CoreManager'; - -/** - * Constructs a new Parse.Error object with the given code and message. - * - * Parse.CoreManager.set('PARSE_ERRORS', [{ code, message }]) can be use to override error messages. - * - * @alias Parse.Error - */ -class ParseError extends Error { - /** - * @param {number} code An error code constant from Parse.Error. - * @param {string} message A detailed description of the error. - */ - constructor(code, message) { - super(message); - this.code = code; - let customMessage = message; - CoreManager.get('PARSE_ERRORS').forEach((error) => { - if (error.code === code && error.code) { - customMessage = error.message; - } - }); - Object.defineProperty(this, 'message', { - enumerable: true, - value: customMessage, - }); - } - - toString() { - return 'ParseError: ' + this.code + ' ' + this.message; - } -} - -/** - * Error code indicating some error other than those enumerated here. - * - * @property {number} OTHER_CAUSE - * @static - */ -ParseError.OTHER_CAUSE = -1; - -/** - * Error code indicating that something has gone wrong with the server. - * - * @property {number} INTERNAL_SERVER_ERROR - * @static - */ -ParseError.INTERNAL_SERVER_ERROR = 1; - -/** - * Error code indicating the connection to the Parse servers failed. - * - * @property {number} CONNECTION_FAILED - * @static - */ -ParseError.CONNECTION_FAILED = 100; - -/** - * Error code indicating the specified object doesn't exist. - * - * @property {number} OBJECT_NOT_FOUND - * @static - */ -ParseError.OBJECT_NOT_FOUND = 101; - -/** - * Error code indicating you tried to query with a datatype that doesn't - * support it, like exact matching an array or object. - * - * @property {number} INVALID_QUERY - * @static - */ -ParseError.INVALID_QUERY = 102; - -/** - * Error code indicating a missing or invalid classname. Classnames are - * case-sensitive. They must start with a letter, and a-zA-Z0-9_ are the - * only valid characters. - * - * @property {number} INVALID_CLASS_NAME - * @static - */ -ParseError.INVALID_CLASS_NAME = 103; - -/** - * Error code indicating an unspecified object id. - * - * @property {number} MISSING_OBJECT_ID - * @static - */ -ParseError.MISSING_OBJECT_ID = 104; - -/** - * Error code indicating an invalid key name. Keys are case-sensitive. They - * must start with a letter, and a-zA-Z0-9_ are the only valid characters. - * - * @property {number} INVALID_KEY_NAME - * @static - */ -ParseError.INVALID_KEY_NAME = 105; - -/** - * Error code indicating a malformed pointer. You should not see this unless - * you have been mucking about changing internal Parse code. - * - * @property {number} INVALID_POINTER - * @static - */ -ParseError.INVALID_POINTER = 106; - -/** - * Error code indicating that badly formed JSON was received upstream. This - * either indicates you have done something unusual with modifying how - * things encode to JSON, or the network is failing badly. - * - * @property {number} INVALID_JSON - * @static - */ -ParseError.INVALID_JSON = 107; - -/** - * Error code indicating that the feature you tried to access is only - * available internally for testing purposes. - * - * @property {number} COMMAND_UNAVAILABLE - * @static - */ -ParseError.COMMAND_UNAVAILABLE = 108; - -/** - * You must call Parse.initialize before using the Parse library. - * - * @property {number} NOT_INITIALIZED - * @static - */ -ParseError.NOT_INITIALIZED = 109; - -/** - * Error code indicating that a field was set to an inconsistent type. - * - * @property {number} INCORRECT_TYPE - * @static - */ -ParseError.INCORRECT_TYPE = 111; - -/** - * Error code indicating an invalid channel name. A channel name is either - * an empty string (the broadcast channel) or contains only a-zA-Z0-9_ - * characters and starts with a letter. - * - * @property {number} INVALID_CHANNEL_NAME - * @static - */ -ParseError.INVALID_CHANNEL_NAME = 112; - -/** - * Error code indicating that push is misconfigured. - * - * @property {number} PUSH_MISCONFIGURED - * @static - */ -ParseError.PUSH_MISCONFIGURED = 115; - -/** - * Error code indicating that the object is too large. - * - * @property {number} OBJECT_TOO_LARGE - * @static - */ -ParseError.OBJECT_TOO_LARGE = 116; - -/** - * Error code indicating that the operation isn't allowed for clients. - * - * @property {number} OPERATION_FORBIDDEN - * @static - */ -ParseError.OPERATION_FORBIDDEN = 119; - -/** - * Error code indicating the result was not found in the cache. - * - * @property {number} CACHE_MISS - * @static - */ -ParseError.CACHE_MISS = 120; - -/** - * Error code indicating that an invalid key was used in a nested - * JSONObject. - * - * @property {number} INVALID_NESTED_KEY - * @static - */ -ParseError.INVALID_NESTED_KEY = 121; - -/** - * Error code indicating that an invalid filename was used for ParseFile. - * A valid file name contains only a-zA-Z0-9_. characters and is between 1 - * and 128 characters. - * - * @property {number} INVALID_FILE_NAME - * @static - */ -ParseError.INVALID_FILE_NAME = 122; - -/** - * Error code indicating an invalid ACL was provided. - * - * @property {number} INVALID_ACL - * @static - */ -ParseError.INVALID_ACL = 123; - -/** - * Error code indicating that the request timed out on the server. Typically - * this indicates that the request is too expensive to run. - * - * @property {number} TIMEOUT - * @static - */ -ParseError.TIMEOUT = 124; - -/** - * Error code indicating that the email address was invalid. - * - * @property {number} INVALID_EMAIL_ADDRESS - * @static - */ -ParseError.INVALID_EMAIL_ADDRESS = 125; - -/** - * Error code indicating a missing content type. - * - * @property {number} MISSING_CONTENT_TYPE - * @static - */ -ParseError.MISSING_CONTENT_TYPE = 126; - -/** - * Error code indicating a missing content length. - * - * @property {number} MISSING_CONTENT_LENGTH - * @static - */ -ParseError.MISSING_CONTENT_LENGTH = 127; - -/** - * Error code indicating an invalid content length. - * - * @property {number} INVALID_CONTENT_LENGTH - * @static - */ -ParseError.INVALID_CONTENT_LENGTH = 128; - -/** - * Error code indicating a file that was too large. - * - * @property {number} FILE_TOO_LARGE - * @static - */ -ParseError.FILE_TOO_LARGE = 129; - -/** - * Error code indicating an error saving a file. - * - * @property {number} FILE_SAVE_ERROR - * @static - */ -ParseError.FILE_SAVE_ERROR = 130; - -/** - * Error code indicating that a unique field was given a value that is - * already taken. - * - * @property {number} DUPLICATE_VALUE - * @static - */ -ParseError.DUPLICATE_VALUE = 137; - -/** - * Error code indicating that a role's name is invalid. - * - * @property {number} INVALID_ROLE_NAME - * @static - */ -ParseError.INVALID_ROLE_NAME = 139; - -/** - * Error code indicating that an application quota was exceeded. Upgrade to - * resolve. - * - * @property {number} EXCEEDED_QUOTA - * @static - */ -ParseError.EXCEEDED_QUOTA = 140; - -/** - * Error code indicating that a Cloud Code script failed. - * - * @property {number} SCRIPT_FAILED - * @static - */ -ParseError.SCRIPT_FAILED = 141; - -/** - * Error code indicating that a Cloud Code validation failed. - * - * @property {number} VALIDATION_ERROR - * @static - */ -ParseError.VALIDATION_ERROR = 142; - -/** - * Error code indicating that invalid image data was provided. - * - * @property {number} INVALID_IMAGE_DATA - * @static - */ -ParseError.INVALID_IMAGE_DATA = 143; - -/** - * Error code indicating an unsaved file. - * - * @property {number} UNSAVED_FILE_ERROR - * @static - */ -ParseError.UNSAVED_FILE_ERROR = 151; - -/** - * Error code indicating an invalid push time. - * - * @property {number} INVALID_PUSH_TIME_ERROR - * @static - */ -ParseError.INVALID_PUSH_TIME_ERROR = 152; - -/** - * Error code indicating an error deleting a file. - * - * @property {number} FILE_DELETE_ERROR - * @static - */ -ParseError.FILE_DELETE_ERROR = 153; - -/** - * Error code indicating an error deleting an unnamed file. - * - * @property {number} FILE_DELETE_UNNAMED_ERROR - * @static - */ -ParseError.FILE_DELETE_UNNAMED_ERROR = 161; - -/** - * Error code indicating that the application has exceeded its request - * limit. - * - * @property {number} REQUEST_LIMIT_EXCEEDED - * @static - */ -ParseError.REQUEST_LIMIT_EXCEEDED = 155; - -/** - * Error code indicating that the request was a duplicate and has been discarded due to - * idempotency rules. - * - * @property {number} DUPLICATE_REQUEST - * @static - */ -ParseError.DUPLICATE_REQUEST = 159; - -/** - * Error code indicating an invalid event name. - * - * @property {number} INVALID_EVENT_NAME - * @static - */ -ParseError.INVALID_EVENT_NAME = 160; - -/** - * Error code indicating that a field had an invalid value. - * - * @property {number} INVALID_VALUE - * @static - */ -ParseError.INVALID_VALUE = 162; - -/** - * Error code indicating that the username is missing or empty. - * - * @property {number} USERNAME_MISSING - * @static - */ -ParseError.USERNAME_MISSING = 200; - -/** - * Error code indicating that the password is missing or empty. - * - * @property {number} PASSWORD_MISSING - * @static - */ -ParseError.PASSWORD_MISSING = 201; - -/** - * Error code indicating that the username has already been taken. - * - * @property {number} USERNAME_TAKEN - * @static - */ -ParseError.USERNAME_TAKEN = 202; - -/** - * Error code indicating that the email has already been taken. - * - * @property {number} EMAIL_TAKEN - * @static - */ -ParseError.EMAIL_TAKEN = 203; - -/** - * Error code indicating that the email is missing, but must be specified. - * - * @property {number} EMAIL_MISSING - * @static - */ -ParseError.EMAIL_MISSING = 204; - -/** - * Error code indicating that a user with the specified email was not found. - * - * @property {number} EMAIL_NOT_FOUND - * @static - */ -ParseError.EMAIL_NOT_FOUND = 205; - -/** - * Error code indicating that a user object without a valid session could - * not be altered. - * - * @property {number} SESSION_MISSING - * @static - */ -ParseError.SESSION_MISSING = 206; - -/** - * Error code indicating that a user can only be created through signup. - * - * @property {number} MUST_CREATE_USER_THROUGH_SIGNUP - * @static - */ -ParseError.MUST_CREATE_USER_THROUGH_SIGNUP = 207; - -/** - * Error code indicating that an an account being linked is already linked - * to another user. - * - * @property {number} ACCOUNT_ALREADY_LINKED - * @static - */ -ParseError.ACCOUNT_ALREADY_LINKED = 208; - -/** - * Error code indicating that the current session token is invalid. - * - * @property {number} INVALID_SESSION_TOKEN - * @static - */ -ParseError.INVALID_SESSION_TOKEN = 209; - -/** - * Error code indicating an error enabling or verifying MFA - * - * @property {number} MFA_ERROR - * @static - */ -ParseError.MFA_ERROR = 210; - -/** - * Error code indicating that a valid MFA token must be provided - * - * @property {number} MFA_TOKEN_REQUIRED - * @static - */ -ParseError.MFA_TOKEN_REQUIRED = 211; - -/** - * Error code indicating that a user cannot be linked to an account because - * that account's id could not be found. - * - * @property {number} LINKED_ID_MISSING - * @static - */ -ParseError.LINKED_ID_MISSING = 250; - -/** - * Error code indicating that a user with a linked (e.g. Facebook) account - * has an invalid session. - * - * @property {number} INVALID_LINKED_SESSION - * @static - */ -ParseError.INVALID_LINKED_SESSION = 251; - -/** - * Error code indicating that a service being linked (e.g. Facebook or - * Twitter) is unsupported. - * - * @property {number} UNSUPPORTED_SERVICE - * @static - */ -ParseError.UNSUPPORTED_SERVICE = 252; - -/** - * Error code indicating an invalid operation occured on schema - * - * @property {number} INVALID_SCHEMA_OPERATION - * @static - */ -ParseError.INVALID_SCHEMA_OPERATION = 255; - -/** - * Error code indicating that there were multiple errors. Aggregate errors - * have an "errors" property, which is an array of error objects with more - * detail about each error that occurred. - * - * @property {number} AGGREGATE_ERROR - * @static - */ -ParseError.AGGREGATE_ERROR = 600; - -/** - * Error code indicating the client was unable to read an input file. - * - * @property {number} FILE_READ_ERROR - * @static - */ -ParseError.FILE_READ_ERROR = 601; - -/** - * Error code indicating a real error code is unavailable because - * we had to use an XDomainRequest object to allow CORS requests in - * Internet Explorer, which strips the body from HTTP responses that have - * a non-2XX status code. - * - * @property {number} X_DOMAIN_REQUEST - * @static - */ -ParseError.X_DOMAIN_REQUEST = 602; - -export default ParseError; diff --git a/src/ParseError.ts b/src/ParseError.ts new file mode 100644 index 000000000..1f8e4e0b1 --- /dev/null +++ b/src/ParseError.ts @@ -0,0 +1,559 @@ +import CoreManager from './CoreManager'; +import type ParseObject from './ParseObject'; + +/** + * Constructs a new Parse.Error object with the given code and message. + * + * Parse.CoreManager.set('PARSE_ERRORS', [{ code, message }]) can be use to override error messages. + * + * @alias Parse.Error + */ +class ParseError extends Error { + code: number; + message: string; + /** In case an error is associated with an object */ + object?: ParseObject; + /** In case of aggregate errors, this is populated */ + errors?: Error[]; + /** + * @param {number} code An error code constant from Parse.Error. + * @param {string} message A detailed description of the error. + */ + constructor(code: number, message?: string) { + super(message); + this.code = code; + let customMessage = message; + CoreManager.get('PARSE_ERRORS').forEach((error: { code: number, message: string }) => { + if (error.code === code && error.code) { + customMessage = error.message; + } + }); + Object.defineProperty(this, 'message', { + enumerable: true, + value: customMessage, + }); + } + + toString() { + return 'ParseError: ' + this.code + ' ' + this.message; + } + + + /** + * Error code indicating some error other than those enumerated here. + * + * @property {number} OTHER_CAUSE + * @static + */ + static OTHER_CAUSE = -1; + + /** + * Error code indicating that something has gone wrong with the server. + * + * @property {number} INTERNAL_SERVER_ERROR + * @static + */ + static INTERNAL_SERVER_ERROR = 1; + + /** + * Error code indicating the connection to the Parse servers failed. + * + * @property {number} CONNECTION_FAILED + * @static + */ + static CONNECTION_FAILED = 100; + + /** + * Error code indicating the specified object doesn't exist. + * + * @property {number} OBJECT_NOT_FOUND + * @static + */ + static OBJECT_NOT_FOUND = 101; + + /** + * Error code indicating you tried to query with a datatype that doesn't + * support it, like exact matching an array or object. + * + * @property {number} INVALID_QUERY + * @static + */ + static INVALID_QUERY = 102; + + /** + * Error code indicating a missing or invalid classname. Classnames are + * case-sensitive. They must start with a letter, and a-zA-Z0-9_ are the + * only valid characters. + * + * @property {number} INVALID_CLASS_NAME + * @static + */ + static INVALID_CLASS_NAME = 103; + + /** + * Error code indicating an unspecified object id. + * + * @property {number} MISSING_OBJECT_ID + * @static + */ + static MISSING_OBJECT_ID = 104; + + /** + * Error code indicating an invalid key name. Keys are case-sensitive. They + * must start with a letter, and a-zA-Z0-9_ are the only valid characters. + * + * @property {number} INVALID_KEY_NAME + * @static + */ + static INVALID_KEY_NAME = 105; + + /** + * Error code indicating a malformed pointer. You should not see this unless + * you have been mucking about changing internal Parse code. + * + * @property {number} INVALID_POINTER + * @static + */ + static INVALID_POINTER = 106; + + /** + * Error code indicating that badly formed JSON was received upstream. This + * either indicates you have done something unusual with modifying how + * things encode to JSON, or the network is failing badly. + * + * @property {number} INVALID_JSON + * @static + */ + static INVALID_JSON = 107; + + /** + * Error code indicating that the feature you tried to access is only + * available internally for testing purposes. + * + * @property {number} COMMAND_UNAVAILABLE + * @static + */ + static COMMAND_UNAVAILABLE = 108; + + /** + * You must call Parse.initialize before using the Parse library. + * + * @property {number} NOT_INITIALIZED + * @static + */ + static NOT_INITIALIZED = 109; + + /** + * Error code indicating that a field was set to an inconsistent type. + * + * @property {number} INCORRECT_TYPE + * @static + */ + static INCORRECT_TYPE = 111; + + /** + * Error code indicating an invalid channel name. A channel name is either + * an empty string (the broadcast channel) or contains only a-zA-Z0-9_ + * characters and starts with a letter. + * + * @property {number} INVALID_CHANNEL_NAME + * @static + */ + static INVALID_CHANNEL_NAME = 112; + + /** + * Error code indicating that push is misconfigured. + * + * @property {number} PUSH_MISCONFIGURED + * @static + */ + static PUSH_MISCONFIGURED = 115; + + /** + * Error code indicating that the object is too large. + * + * @property {number} OBJECT_TOO_LARGE + * @static + */ + static OBJECT_TOO_LARGE = 116; + + /** + * Error code indicating that the operation isn't allowed for clients. + * + * @property {number} OPERATION_FORBIDDEN + * @static + */ + static OPERATION_FORBIDDEN = 119; + + /** + * Error code indicating the result was not found in the cache. + * + * @property {number} CACHE_MISS + * @static + */ + static CACHE_MISS = 120; + + /** + * Error code indicating that an invalid key was used in a nested + * JSONObject. + * + * @property {number} INVALID_NESTED_KEY + * @static + */ + static INVALID_NESTED_KEY = 121; + + /** + * Error code indicating that an invalid filename was used for ParseFile. + * A valid file name contains only a-zA-Z0-9_. characters and is between 1 + * and 128 characters. + * + * @property {number} INVALID_FILE_NAME + * @static + */ + static INVALID_FILE_NAME = 122; + + /** + * Error code indicating an invalid ACL was provided. + * + * @property {number} INVALID_ACL + * @static + */ + static INVALID_ACL = 123; + + /** + * Error code indicating that the request timed out on the server. Typically + * this indicates that the request is too expensive to run. + * + * @property {number} TIMEOUT + * @static + */ + static TIMEOUT = 124; + + /** + * Error code indicating that the email address was invalid. + * + * @property {number} INVALID_EMAIL_ADDRESS + * @static + */ + static INVALID_EMAIL_ADDRESS = 125; + + /** + * Error code indicating a missing content type. + * + * @property {number} MISSING_CONTENT_TYPE + * @static + */ + static MISSING_CONTENT_TYPE = 126; + + /** + * Error code indicating a missing content length. + * + * @property {number} MISSING_CONTENT_LENGTH + * @static + */ + static MISSING_CONTENT_LENGTH = 127; + + /** + * Error code indicating an invalid content length. + * + * @property {number} INVALID_CONTENT_LENGTH + * @static + */ + static INVALID_CONTENT_LENGTH = 128; + + /** + * Error code indicating a file that was too large. + * + * @property {number} FILE_TOO_LARGE + * @static + */ + static FILE_TOO_LARGE = 129; + + /** + * Error code indicating an error saving a file. + * + * @property {number} FILE_SAVE_ERROR + * @static + */ + static FILE_SAVE_ERROR = 130; + + /** + * Error code indicating that a unique field was given a value that is + * already taken. + * + * @property {number} DUPLICATE_VALUE + * @static + */ + static DUPLICATE_VALUE = 137; + + /** + * Error code indicating that a role's name is invalid. + * + * @property {number} INVALID_ROLE_NAME + * @static + */ + static INVALID_ROLE_NAME = 139; + + /** + * Error code indicating that an application quota was exceeded. Upgrade to + * resolve. + * + * @property {number} EXCEEDED_QUOTA + * @static + */ + static EXCEEDED_QUOTA = 140; + + /** + * Error code indicating that a Cloud Code script failed. + * + * @property {number} SCRIPT_FAILED + * @static + */ + static SCRIPT_FAILED = 141; + + /** + * Error code indicating that a Cloud Code validation failed. + * + * @property {number} VALIDATION_ERROR + * @static + */ + static VALIDATION_ERROR = 142; + + /** + * Error code indicating that invalid image data was provided. + * + * @property {number} INVALID_IMAGE_DATA + * @static + */ + static INVALID_IMAGE_DATA = 143; + + /** + * Error code indicating an unsaved file. + * + * @property {number} UNSAVED_FILE_ERROR + * @static + */ + static UNSAVED_FILE_ERROR = 151; + + /** + * Error code indicating an invalid push time. + * + * @property {number} INVALID_PUSH_TIME_ERROR + * @static + */ + static INVALID_PUSH_TIME_ERROR = 152; + + /** + * Error code indicating an error deleting a file. + * + * @property {number} FILE_DELETE_ERROR + * @static + */ + static FILE_DELETE_ERROR = 153; + + /** + * Error code indicating an error deleting an unnamed file. + * + * @property {number} FILE_DELETE_UNNAMED_ERROR + * @static + */ + static FILE_DELETE_UNNAMED_ERROR = 161; + + /** + * Error code indicating that the application has exceeded its request + * limit. + * + * @property {number} REQUEST_LIMIT_EXCEEDED + * @static + */ + static REQUEST_LIMIT_EXCEEDED = 155; + + /** + * Error code indicating that the request was a duplicate and has been discarded due to + * idempotency rules. + * + * @property {number} DUPLICATE_REQUEST + * @static + */ + static DUPLICATE_REQUEST = 159; + + /** + * Error code indicating an invalid event name. + * + * @property {number} INVALID_EVENT_NAME + * @static + */ + static INVALID_EVENT_NAME = 160; + + /** + * Error code indicating that a field had an invalid value. + * + * @property {number} INVALID_VALUE + * @static + */ + static INVALID_VALUE = 162; + + /** + * Error code indicating that the username is missing or empty. + * + * @property {number} USERNAME_MISSING + * @static + */ + static USERNAME_MISSING = 200; + + /** + * Error code indicating that the password is missing or empty. + * + * @property {number} PASSWORD_MISSING + * @static + */ + static PASSWORD_MISSING = 201; + + /** + * Error code indicating that the username has already been taken. + * + * @property {number} USERNAME_TAKEN + * @static + */ + static USERNAME_TAKEN = 202; + + /** + * Error code indicating that the email has already been taken. + * + * @property {number} EMAIL_TAKEN + * @static + */ + static EMAIL_TAKEN = 203; + + /** + * Error code indicating that the email is missing, but must be specified. + * + * @property {number} EMAIL_MISSING + * @static + */ + static EMAIL_MISSING = 204; + + /** + * Error code indicating that a user with the specified email was not found. + * + * @property {number} EMAIL_NOT_FOUND + * @static + */ + static EMAIL_NOT_FOUND = 205; + + /** + * Error code indicating that a user object without a valid session could + * not be altered. + * + * @property {number} SESSION_MISSING + * @static + */ + static SESSION_MISSING = 206; + + /** + * Error code indicating that a user can only be created through signup. + * + * @property {number} MUST_CREATE_USER_THROUGH_SIGNUP + * @static + */ + static MUST_CREATE_USER_THROUGH_SIGNUP = 207; + + /** + * Error code indicating that an an account being linked is already linked + * to another user. + * + * @property {number} ACCOUNT_ALREADY_LINKED + * @static + */ + static ACCOUNT_ALREADY_LINKED = 208; + + /** + * Error code indicating that the current session token is invalid. + * + * @property {number} INVALID_SESSION_TOKEN + * @static + */ + static INVALID_SESSION_TOKEN = 209; + + /** + * Error code indicating an error enabling or verifying MFA + * + * @property {number} MFA_ERROR + * @static + */ + static MFA_ERROR = 210; + + /** + * Error code indicating that a valid MFA token must be provided + * + * @property {number} MFA_TOKEN_REQUIRED + * @static + */ + static MFA_TOKEN_REQUIRED = 211; + + /** + * Error code indicating that a user cannot be linked to an account because + * that account's id could not be found. + * + * @property {number} LINKED_ID_MISSING + * @static + */ + static LINKED_ID_MISSING = 250; + + /** + * Error code indicating that a user with a linked (e.g. Facebook) account + * has an invalid session. + * + * @property {number} INVALID_LINKED_SESSION + * @static + */ + static INVALID_LINKED_SESSION = 251; + + /** + * Error code indicating that a service being linked (e.g. Facebook or + * Twitter) is unsupported. + * + * @property {number} UNSUPPORTED_SERVICE + * @static + */ + static UNSUPPORTED_SERVICE = 252; + + /** + * Error code indicating an invalid operation occured on schema + * + * @property {number} INVALID_SCHEMA_OPERATION + * @static + */ + static INVALID_SCHEMA_OPERATION = 255; + + /** + * Error code indicating that there were multiple errors. Aggregate errors + * have an "errors" property, which is an array of error objects with more + * detail about each error that occurred. + * + * @property {number} AGGREGATE_ERROR + * @static + */ + static AGGREGATE_ERROR = 600; + + /** + * Error code indicating the client was unable to read an input file. + * + * @property {number} FILE_READ_ERROR + * @static + */ + static FILE_READ_ERROR = 601; + + /** + * Error code indicating a real error code is unavailable because + * we had to use an XDomainRequest object to allow CORS requests in + * Internet Explorer, which strips the body from HTTP responses that have + * a non-2XX status code. + * + * @property {number} X_DOMAIN_REQUEST + * @static + */ + static X_DOMAIN_REQUEST = 602; +} + +export default ParseError; \ No newline at end of file diff --git a/src/ParseFile.js b/src/ParseFile.ts similarity index 86% rename from src/ParseFile.js rename to src/ParseFile.ts index 1d7ff6945..cd397c8af 100644 --- a/src/ParseFile.js +++ b/src/ParseFile.ts @@ -4,10 +4,9 @@ /* global XMLHttpRequest, Blob */ import CoreManager from './CoreManager'; import type { FullOptions } from './RESTController'; +import ParseError from './ParseError' -const ParseError = require('./ParseError').default; - -let XHR = null; +let XHR: typeof XMLHttpRequest = null as any; if (typeof XMLHttpRequest !== 'undefined') { XHR = XMLHttpRequest; } @@ -20,22 +19,22 @@ type Uri = { uri: string }; type FileData = Array | Base64 | Blob | Uri; export type FileSource = | { - format: 'file', - file: Blob, - type: string, - } + format: 'file', + file: Blob, + type: string, + } | { - format: 'base64', - base64: string, - type: string, - } + format: 'base64', + base64: string, + type?: string, + } | { - format: 'uri', - uri: string, - type: string, - }; + format: 'uri', + uri: string, + type: string, + }; -function b64Digit(number: number): string { +export function b64Digit(number: number): string { if (number < 26) { return String.fromCharCode(65 + number); } @@ -54,6 +53,11 @@ function b64Digit(number: number): string { throw new TypeError('Tried to encode large digit ' + number + ' in base64.'); } +export type FileSaveOptions = FullOptions & { + metadata?: { [key: string]: any } + tags?: { [key: string]: any } +} + /** * A Parse.File is a local representation of a file that is saved to the Parse * cloud. @@ -62,13 +66,13 @@ function b64Digit(number: number): string { */ class ParseFile { _name: string; - _url: ?string; + _url?: string; _source: FileSource; - _previousSave: ?Promise; - _data: ?string; - _requestTask: ?any; - _metadata: ?Object; - _tags: ?Object; + _previousSave?: Promise; + _data?: string; + _requestTask?: any; + _metadata: Object; + _tags: Object; /** * @param name {String} The file's name. This will be prefixed by a unique @@ -121,17 +125,18 @@ class ParseFile { file: data, type: specifiedType, }; - } else if (data && typeof data.uri === 'string' && data.uri !== undefined) { + } else if (data && typeof (data as Uri).uri === 'string') { this._source = { format: 'uri', - uri: data.uri, + uri: (data as Uri).uri, type: specifiedType, }; - } else if (data && typeof data.base64 === 'string') { - const base64 = data.base64.split(',').slice(-1)[0]; + } else if (data && typeof (data as Base64).base64 === 'string') { + const b64data = data as Base64; + const base64 = b64data.base64.split(',').slice(-1)[0]; const dataType = specifiedType || - data.base64.split(';').slice(0, 1)[0].split(':').slice(1, 2)[0] || + b64data.base64.split(';').slice(0, 1)[0].split(':').slice(1, 2)[0] || 'text/plain'; this._data = base64; this._source = { @@ -152,7 +157,7 @@ class ParseFile { * * @returns {Promise} Promise that is resolve with base64 data */ - async getData(): Promise { + async getData(): Promise { if (this._data) { return this._data; } @@ -165,7 +170,7 @@ class ParseFile { const controller = CoreManager.getFileController(); const result = await controller.download(this._url, options); this._data = result.base64; - return this._data; + return this._data!; } /** @@ -186,7 +191,7 @@ class ParseFile { * @param {object} options An object to specify url options * @returns {string | undefined} */ - url(options?: { forceSecure?: boolean }): ?string { + url(options?: { forceSecure?: boolean }): string | undefined { options = options || {}; if (!this._url) { return; @@ -203,7 +208,7 @@ class ParseFile { * * @returns {object} */ - metadata(): Object { + metadata() { return this._metadata; } @@ -212,7 +217,7 @@ class ParseFile { * * @returns {object} */ - tags(): Object { + tags() { return this._tags; } @@ -239,9 +244,9 @@ class ParseFile { * * @returns {Promise | undefined} Promise that is resolved when the save finishes. */ - save(options?: FullOptions): ?Promise { - options = options || {}; - options.requestTask = task => (this._requestTask = task); + save(options?: FileSaveOptions): Promise | undefined { + options = { ...options || {} }; + options.requestTask = (task: any) => (this._requestTask = task); options.metadata = this._metadata; options.tags = this._tags; @@ -251,7 +256,7 @@ class ParseFile { this._previousSave = controller.saveFile(this._name, this._source, options).then(res => { this._name = res.name; this._url = res.url; - this._data = null; + this._data = undefined; this._requestTask = null; return this; }); @@ -263,16 +268,16 @@ class ParseFile { return {}; } const newSource = { - format: 'base64', + format: 'base64' as const, base64: result.base64, - type: result.contentType, + type: result.contentType!, }; this._data = result.base64; this._requestTask = null; return controller.saveBase64(this._name, newSource, options); }) - .then(res => { - this._name = res.name; + .then((res: { name?: string, url?: string }) => { + this._name = res.name!; this._url = res.url; this._requestTask = null; return this; @@ -313,23 +318,23 @@ class ParseFile { *
    * @returns {Promise} Promise that is resolved when the delete finishes.
    */
-  destroy(options?: FullOptions = {}) {
+  destroy(options: FullOptions = {}) {
     if (!this._name) {
       throw new ParseError(ParseError.FILE_DELETE_UNNAMED_ERROR, 'Cannot delete an unnamed file.');
     }
     const destroyOptions = { useMasterKey: true };
     if (options.hasOwnProperty('useMasterKey')) {
-      destroyOptions.useMasterKey = options.useMasterKey;
+      destroyOptions.useMasterKey = options.useMasterKey!;
     }
     const controller = CoreManager.getFileController();
     return controller.deleteFile(this._name, destroyOptions).then(() => {
-      this._data = null;
+      this._data = undefined;
       this._requestTask = null;
       return this;
     });
   }
 
-  toJSON(): { name: ?string, url: ?string } {
+  toJSON(): { __type: 'File', name?: string, url?: string } {
     return {
       __type: 'File',
       name: this._name,
@@ -337,7 +342,7 @@ class ParseFile {
     };
   }
 
-  equals(other: mixed): boolean {
+  equals(other: any): boolean {
     if (this === other) {
       return true;
     }
@@ -409,8 +414,8 @@ class ParseFile {
     return file;
   }
 
-  static encodeBase64(bytes: Array): string {
-    const chunks = [];
+  static encodeBase64(bytes: Array | Uint8Array): string {
+    const chunks: string[] = [];
     chunks.length = Math.ceil(bytes.length / 3);
     for (let i = 0; i < chunks.length; i++) {
       const b1 = bytes[i * 3];
@@ -437,10 +442,10 @@ const DefaultController = {
     if (source.format !== 'file') {
       throw new Error('saveFile can only be used with File-type sources.');
     }
-    const base64Data = await new Promise((res, rej) => {
+    const base64Data = await new Promise((res, rej) => {
       // eslint-disable-next-line no-undef
       const reader = new FileReader();
-      reader.onload = () => res(reader.result);
+      reader.onload = () => res(reader.result as string);
       reader.onerror = error => rej(error);
       reader.readAsDataURL(source.file);
     });
@@ -451,14 +456,14 @@ const DefaultController = {
     // use the entire string instead
     const data = second ? second : first;
     const newSource = {
-      format: 'base64',
+      format: 'base64' as const,
       base64: data,
-      type: source.type || (source.file ? source.file.type : null),
+      type: source.type || (source.file ? source.file.type : undefined),
     };
     return await DefaultController.saveBase64(name, newSource, options);
   },
 
-  saveBase64: function (name: string, source: FileSource, options?: FullOptions) {
+  saveBase64: function (name: string, source: FileSource, options: FileSaveOptions = {}) {
     if (source.format !== 'base64') {
       throw new Error('saveBase64 can only be used with Base64-type sources.');
     }
@@ -469,16 +474,17 @@ const DefaultController = {
         tags: { ...options.tags },
       },
     };
-    delete options.metadata;
-    delete options.tags;
+    const restOptions = { ...options };
+    delete restOptions.metadata;
+    delete restOptions.tags;
     if (source.type) {
       data._ContentType = source.type;
     }
     const path = 'files/' + name;
-    return CoreManager.getRESTController().request('POST', path, data, options);
+    return CoreManager.getRESTController().request('POST', path, data, restOptions);
   },
 
-  download: function (uri, options) {
+  download: function (uri: string, options: any) {
     if (XHR) {
       return this.downloadAjax(uri, options);
     } else if (process.env.PARSE_BUILD === 'node') {
@@ -506,7 +512,7 @@ const DefaultController = {
     }
   },
 
-  downloadAjax: function (uri, options) {
+  downloadAjax: function (uri: string, options: any) {
     return new Promise((resolve, reject) => {
       const xhr = new XHR();
       xhr.open('GET', uri, true);
@@ -536,7 +542,7 @@ const DefaultController = {
     const headers = {
       'X-Parse-Application-ID': CoreManager.get('APPLICATION_ID'),
     };
-    if (options.useMasterKey) {
+    if (options?.useMasterKey) {
       headers['X-Parse-Master-Key'] = CoreManager.get('MASTER_KEY');
     }
     let url = CoreManager.get('SERVER_URL');
@@ -568,4 +574,3 @@ const DefaultController = {
 CoreManager.setFileController(DefaultController);
 
 export default ParseFile;
-exports.b64Digit = b64Digit;
diff --git a/src/ParseGeoPoint.js b/src/ParseGeoPoint.ts
similarity index 94%
rename from src/ParseGeoPoint.js
rename to src/ParseGeoPoint.ts
index 3af089e98..95cdd8e95 100644
--- a/src/ParseGeoPoint.js
+++ b/src/ParseGeoPoint.ts
@@ -102,7 +102,7 @@ class ParseGeoPoint {
     };
   }
 
-  equals(other: mixed): boolean {
+  equals(other: any): boolean {
     return (
       other instanceof ParseGeoPoint &&
       this.latitude === other.latitude &&
@@ -186,6 +186,9 @@ class ParseGeoPoint {
    * @static
    * @returns {Parse.GeoPoint} User's current location
    */
+  // TODO: Typescript; How does this thing work?
+  // Seems we're using the power of Javascript by returning a value from a synchronous callback, so the value ends up correct somehow in tests?
+  // Should this be `async` instead for safety? Since it's a callback pattern
   static current() {
     return navigator.geolocation.getCurrentPosition(location => {
       return new ParseGeoPoint(location.coords.latitude, location.coords.longitude);
diff --git a/src/ParseHooks.js b/src/ParseHooks.ts
similarity index 54%
rename from src/ParseHooks.js
rename to src/ParseHooks.ts
index 8a5982687..c6edf219c 100644
--- a/src/ParseHooks.js
+++ b/src/ParseHooks.ts
@@ -10,52 +10,54 @@ export function getTriggers() {
   return CoreManager.getHooksController().get('triggers');
 }
 
-export function getFunction(name) {
+export function getFunction(name: string) {
   return CoreManager.getHooksController().get('functions', name);
 }
 
-export function getTrigger(className, triggerName) {
+export function getTrigger(className: string, triggerName: string) {
   return CoreManager.getHooksController().get('triggers', className, triggerName);
 }
 
-export function createFunction(functionName, url) {
+export function createFunction(functionName: string, url: string) {
   return create({ functionName: functionName, url: url });
 }
 
-export function createTrigger(className, triggerName, url) {
+export function createTrigger(className: string, triggerName: string, url: string) {
   return create({ className: className, triggerName: triggerName, url: url });
 }
 
-export function create(hook) {
+export function create(hook: HookDeclaration) {
   return CoreManager.getHooksController().create(hook);
 }
 
-export function updateFunction(functionName, url) {
+export function updateFunction(functionName: string, url: string) {
   return update({ functionName: functionName, url: url });
 }
 
-export function updateTrigger(className, triggerName, url) {
+export function updateTrigger(className: string, triggerName: string, url: string) {
   return update({ className: className, triggerName: triggerName, url: url });
 }
 
-export function update(hook) {
+export function update(hook: HookDeclaration) {
   return CoreManager.getHooksController().update(hook);
 }
 
-export function removeFunction(functionName) {
+export function removeFunction(functionName: string) {
   return remove({ functionName: functionName });
 }
 
-export function removeTrigger(className, triggerName) {
+export function removeTrigger(className: string, triggerName: string) {
   return remove({ className: className, triggerName: triggerName });
 }
 
-export function remove(hook) {
+export function remove(hook: HookDeleteArg) {
   return CoreManager.getHooksController().remove(hook);
 }
 
+export type HookDeclaration = { functionName: string, url: string } | { className: string, triggerName: string, url: string };
+export type HookDeleteArg = { functionName: string } | { className: string, triggerName: string };
 const DefaultController = {
-  get(type, functionName, triggerName) {
+  get(type: string, functionName?: string, triggerName?: string) {
     let url = '/hooks/' + type;
     if (functionName) {
       url += '/' + functionName;
@@ -66,11 +68,11 @@ const DefaultController = {
     return this.sendRequest('GET', url);
   },
 
-  create(hook) {
-    let url;
-    if (hook.functionName && hook.url) {
+  create(hook: HookDeclaration) {
+    let url: string;
+    if ('functionName' in hook && hook.url) {
       url = '/hooks/functions';
-    } else if (hook.className && hook.triggerName && hook.url) {
+    } else if ('className' in hook && hook.triggerName && hook.url) {
       url = '/hooks/triggers';
     } else {
       return Promise.reject({ error: 'invalid hook declaration', code: 143 });
@@ -78,43 +80,45 @@ const DefaultController = {
     return this.sendRequest('POST', url, hook);
   },
 
-  remove(hook) {
-    let url;
-    if (hook.functionName) {
+  remove(hook: { functionName: string } | { className: string, triggerName: string }) {
+    let url: string;
+    const putParams = { ...hook };
+    if ('functionName' in hook) {
       url = '/hooks/functions/' + hook.functionName;
-      delete hook.functionName;
+      delete (putParams as Partial).functionName;
     } else if (hook.className && hook.triggerName) {
       url = '/hooks/triggers/' + hook.className + '/' + hook.triggerName;
-      delete hook.className;
-      delete hook.triggerName;
+      delete (putParams as Partial).className;
+      delete (putParams as Partial).triggerName;
     } else {
       return Promise.reject({ error: 'invalid hook declaration', code: 143 });
     }
     return this.sendRequest('PUT', url, { __op: 'Delete' });
   },
 
-  update(hook) {
-    let url;
-    if (hook.functionName && hook.url) {
+  update(hook: HookDeclaration) {
+    let url: string;
+    const postParams = { ...hook };
+    if ('functionName' in hook && hook.url) {
       url = '/hooks/functions/' + hook.functionName;
-      delete hook.functionName;
-    } else if (hook.className && hook.triggerName && hook.url) {
+      delete (postParams as Partial).functionName;
+    } else if ('className' in hook && hook.triggerName && hook.url) {
       url = '/hooks/triggers/' + hook.className + '/' + hook.triggerName;
-      delete hook.className;
-      delete hook.triggerName;
+      delete (postParams as Partial).className;
+      delete (postParams as Partial).triggerName;
     } else {
       return Promise.reject({ error: 'invalid hook declaration', code: 143 });
     }
-    return this.sendRequest('PUT', url, hook);
+    return this.sendRequest('PUT', url, postParams);
   },
 
-  sendRequest(method, url, body) {
+  sendRequest(method: string, url: string, body?: any) {
     return CoreManager.getRESTController()
       .request(method, url, body, { useMasterKey: true })
       .then(res => {
         const decoded = decode(res);
         if (decoded) {
-          return Promise.resolve(decoded);
+          return Promise.resolve(decoded);
         }
         return Promise.reject(
           new ParseError(ParseError.INVALID_JSON, 'The server returned an invalid response.')
diff --git a/src/ParseInstallation.js b/src/ParseInstallation.ts
similarity index 91%
rename from src/ParseInstallation.js
rename to src/ParseInstallation.ts
index 380b5c8a9..c624aa27f 100644
--- a/src/ParseInstallation.js
+++ b/src/ParseInstallation.ts
@@ -7,7 +7,7 @@ import ParseObject from './ParseObject';
 import type { AttributeMap } from './ObjectStateMutations';
 
 export default class Installation extends ParseObject {
-  constructor(attributes: ?AttributeMap) {
+  constructor(attributes?: AttributeMap) {
     super('_Installation');
     if (attributes && typeof attributes === 'object') {
       if (!this.set(attributes || {})) {
diff --git a/src/ParseLiveQuery.js b/src/ParseLiveQuery.ts
similarity index 95%
rename from src/ParseLiveQuery.js
rename to src/ParseLiveQuery.ts
index eea9e59e2..0a507d6a6 100644
--- a/src/ParseLiveQuery.js
+++ b/src/ParseLiveQuery.ts
@@ -3,7 +3,7 @@
  */
 import LiveQueryClient from './LiveQueryClient';
 import CoreManager from './CoreManager';
-
+import type { EventEmitter } from 'events';
 function getLiveQueryClient(): Promise {
   return CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
 }
@@ -36,6 +36,10 @@ function getLiveQueryClient(): Promise {
  * @static
  */
 class LiveQuery {
+  emitter: EventEmitter;
+  on: EventEmitter['on'];
+  emit: EventEmitter['emit'];
+
   constructor() {
     const EventEmitter = CoreManager.getEventEmitter();
     this.emitter = new EventEmitter();
@@ -44,14 +48,14 @@ class LiveQuery {
 
     // adding listener so process does not crash
     // best practice is for developer to register their own listener
-    this.on('error', () => {});
+    this.on('error', () => { });
   }
 
   /**
    * After open is called, the LiveQuery will try to send a connect request
    * to the LiveQuery server.
    */
-  async open(): void {
+  async open() {
     const liveQueryClient = await getLiveQueryClient();
     liveQueryClient.open();
   }
@@ -63,7 +67,7 @@ class LiveQuery {
    * If you call query.subscribe() after this, we'll create a new WebSocket
    * connection to the LiveQuery server.
    */
-  async close(): void {
+  async close() {
     const liveQueryClient = await getLiveQueryClient();
     liveQueryClient.close();
   }
diff --git a/src/ParseObject.js b/src/ParseObject.ts
similarity index 91%
rename from src/ParseObject.js
rename to src/ParseObject.ts
index 32701191b..270a0ba9e 100644
--- a/src/ParseObject.js
+++ b/src/ParseObject.ts
@@ -36,12 +36,13 @@ import unsavedChildren from './unsavedChildren';
 import type { AttributeMap, OpsMap } from './ObjectStateMutations';
 import type { RequestOptions, FullOptions } from './RESTController';
 
-const uuidv4 = require('./uuid');
+import uuidv4 from './uuid';
 
 export type Pointer = {
   __type: string,
   className: string,
-  objectId: string,
+  objectId?: string,
+  _localId?: string
 };
 
 type SaveParams = {
@@ -53,6 +54,7 @@ type SaveParams = {
 export type SaveOptions = FullOptions & {
   cascadeSave?: boolean,
   context?: AttributeMap,
+  batchSize?: number
 };
 
 // Mapping of class names to constructors, so we can populate objects from the
@@ -80,6 +82,10 @@ function getServerUrlPath() {
   return url.substr(url.indexOf('/'));
 }
 
+type ObjectFetchOptions = {
+  useMasterKey?: boolean, sessionToken?: string, include?: string | string[], context?: AttributeMap,
+}
+
 /**
  * Creates a new model with defined attributes.
  *
@@ -105,8 +111,8 @@ class ParseObject {
    * @param {object} options The options for this object instance.
    */
   constructor(
-    className: ?string | { className: string, [attr: string]: mixed },
-    attributes?: { [attr: string]: mixed },
+    className?: string | { className: string, [attr: string]: any },
+    attributes?: { [attr: string]: any },
     options?: { ignoreValidation: boolean }
   ) {
     // Enable legacy initializers
@@ -114,7 +120,7 @@ class ParseObject {
       this.initialize.apply(this, arguments);
     }
 
-    let toSet = null;
+    let toSet: { [attr: string]: any } | null = null;
     this._objCount = objectCount++;
     if (typeof className === 'string') {
       this.className = className;
@@ -130,7 +136,7 @@ class ParseObject {
         }
       }
       if (attributes && typeof attributes === 'object') {
-        options = attributes;
+        options = attributes as any;
       }
     }
     if (toSet && !this.set(toSet, options)) {
@@ -143,10 +149,11 @@ class ParseObject {
    *
    * @property {string} id
    */
-  id: ?string;
-  _localId: ?string;
+  id?: string;
+  _localId?: string;
   _objCount: number;
   className: string;
+  static className: string;
 
   /* Prototype getters / setters */
 
@@ -161,7 +168,7 @@ class ParseObject {
    * @property {Date} createdAt
    * @returns {Date}
    */
-  get createdAt(): ?Date {
+  get createdAt(): Date | undefined {
     return this._getServerData().createdAt;
   }
 
@@ -171,7 +178,7 @@ class ParseObject {
    * @property {Date} updatedAt
    * @returns {Date}
    */
-  get updatedAt(): ?Date {
+  get updatedAt(): Date | undefined {
     return this._getServerData().updatedAt;
   }
 
@@ -280,7 +287,7 @@ class ParseObject {
   }
 
   _toFullJSON(seen?: Array, offline?: boolean): AttributeMap {
-    const json: { [key: string]: mixed } = this.toJSON(seen, offline);
+    const json: { [key: string]: any } = this.toJSON(seen, offline);
     json.__type = 'Object';
     json.className = this.className;
     return json;
@@ -290,7 +297,7 @@ class ParseObject {
     const pending = this._getPendingOps();
     const dirtyObjects = this._getDirtyObjectAttributes();
     const json = {};
-    let attr;
+    let attr: string;
 
     for (attr in dirtyObjects) {
       let isDotNotation = false;
@@ -346,7 +353,12 @@ class ParseObject {
     }
     const stateController = CoreManager.getObjectStateController();
     stateController.initializeState(this._getStateIdentifier());
-    const decoded = {};
+    const decoded: Partial<{
+      createdAt?: Date,
+      updatedAt?: Date,
+      ACL?: any // TODO: Maybe type this better?
+      [key: string]: any
+    }> = {};
     for (const attr in serverData) {
       if (attr === 'ACL') {
         decoded[attr] = new ParseACL(serverData[attr]);
@@ -358,10 +370,10 @@ class ParseObject {
       }
     }
     if (decoded.createdAt && typeof decoded.createdAt === 'string') {
-      decoded.createdAt = parseDate(decoded.createdAt);
+      decoded.createdAt = parseDate(decoded.createdAt) || undefined;
     }
     if (decoded.updatedAt && typeof decoded.updatedAt === 'string') {
-      decoded.updatedAt = parseDate(decoded.updatedAt);
+      decoded.updatedAt = parseDate(decoded.updatedAt) || undefined;
     }
     if (!decoded.updatedAt && decoded.createdAt) {
       decoded.updatedAt = decoded.createdAt;
@@ -395,16 +407,22 @@ class ParseObject {
   }
 
   _handleSaveResponse(response: AttributeMap, status: number) {
-    const changes = {};
+    const changes: Partial<{
+      createdAt: string,
+      updatedAt: string,
+      [key: string]: any
+    }> = {};
     let attr;
     const stateController = CoreManager.getObjectStateController();
     const pending = stateController.popPendingState(this._getStateIdentifier());
-    for (attr in pending) {
-      if (pending[attr] instanceof RelationOp) {
-        changes[attr] = pending[attr].applyTo(undefined, this, attr);
-      } else if (!(attr in response)) {
-        // Only SetOps and UnsetOps should not come back with results
-        changes[attr] = pending[attr].applyTo(undefined);
+    if (pending) {
+      for (attr in pending) {
+        if (pending[attr] instanceof RelationOp) {
+          changes[attr] = pending[attr].applyTo(undefined, this, attr);
+        } else if (!(attr in response)) {
+          // Only SetOps and UnsetOps should not come back with results
+          changes[attr] = pending[attr].applyTo(undefined);
+        }
       }
     }
     for (attr in response) {
@@ -462,7 +480,7 @@ class ParseObject {
   toJSON(seen: Array | void, offline?: boolean): AttributeMap {
     const seenEntry = this.id ? this.className + ':' + this.id : this;
     seen = seen || [seenEntry];
-    const json = {};
+    const json: AttributeMap = {};
     const attrs = this.attributes;
     for (const attr in attrs) {
       if ((attr === 'createdAt' || attr === 'updatedAt') && attrs[attr].toJSON) {
@@ -488,7 +506,7 @@ class ParseObject {
    * @param {object} other - An other object ot compare
    * @returns {boolean}
    */
-  equals(other: mixed): boolean {
+  equals(other: any): boolean {
     if (this === other) {
       return true;
     }
@@ -602,7 +620,7 @@ class ParseObject {
    * @param {string} attr The string name of an attribute.
    * @returns {*}
    */
-  get(attr: string): mixed {
+  get(attr: string): any {
     return this.attributes[attr];
   }
 
@@ -689,7 +707,8 @@ class ParseObject {
    *     The only supported option is error.
    * @returns {(ParseObject|boolean)} true if the set succeeded.
    */
-  set(key: mixed, value: mixed, options?: mixed): ParseObject | boolean {
+  set(key: any, value?: any, options?: any): ParseObject | boolean {
+    // TODO: Improve types here without breaking stuff.
     let changes = {};
     const newOps = {};
     if (key && typeof key === 'object') {
@@ -698,13 +717,15 @@ class ParseObject {
     } else if (typeof key === 'string') {
       changes[key] = value;
     } else {
+      // Key is weird; just return ourself
       return this;
     }
 
     options = options || {};
-    let readonly = [];
-    if (typeof this.constructor.readOnlyAttributes === 'function') {
-      readonly = readonly.concat(this.constructor.readOnlyAttributes());
+    /** Readonly attributes of the object class */
+    let readonly: string[] = [];
+    if (typeof ((this.constructor as any).readOnlyAttributes) === 'function') {
+      readonly = readonly.concat((this.constructor as any).readOnlyAttributes());
     }
     for (const k in changes) {
       if (k === 'createdAt' || k === 'updatedAt') {
@@ -787,7 +808,7 @@ class ParseObject {
    * @param options
    * @returns {(ParseObject | boolean)}
    */
-  unset(attr: string, options?: { [opt: string]: mixed }): ParseObject | boolean {
+  unset(attr: string, options?: { [opt: string]: any }): ParseObject | boolean {
     options = options || {};
     options.unset = true;
     return this.set(attr, null, options);
@@ -837,7 +858,7 @@ class ParseObject {
    * @param item {} The item to add.
    * @returns {(ParseObject | boolean)}
    */
-  add(attr: string, item: mixed): ParseObject | boolean {
+  add(attr: string, item: any): ParseObject | boolean {
     return this.set(attr, new AddOp([item]));
   }
 
@@ -849,7 +870,7 @@ class ParseObject {
    * @param items {Object[]} The items to add.
    * @returns {(ParseObject | boolean)}
    */
-  addAll(attr: string, items: Array): ParseObject | boolean {
+  addAll(attr: string, items: Array): ParseObject | boolean {
     return this.set(attr, new AddOp(items));
   }
 
@@ -862,7 +883,7 @@ class ParseObject {
    * @param item {} The object to add.
    * @returns {(ParseObject | boolean)}
    */
-  addUnique(attr: string, item: mixed): ParseObject | boolean {
+  addUnique(attr: string, item: any): ParseObject | boolean {
     return this.set(attr, new AddUniqueOp([item]));
   }
 
@@ -875,7 +896,7 @@ class ParseObject {
    * @param items {Object[]} The objects to add.
    * @returns {(ParseObject | boolean)}
    */
-  addAllUnique(attr: string, items: Array): ParseObject | boolean {
+  addAllUnique(attr: string, items: Array): ParseObject | boolean {
     return this.set(attr, new AddUniqueOp(items));
   }
 
@@ -887,7 +908,7 @@ class ParseObject {
    * @param item {} The object to remove.
    * @returns {(ParseObject | boolean)}
    */
-  remove(attr: string, item: mixed): ParseObject | boolean {
+  remove(attr: string, item: any): ParseObject | boolean {
     return this.set(attr, new RemoveOp([item]));
   }
 
@@ -899,7 +920,7 @@ class ParseObject {
    * @param items {Object[]} The object to remove.
    * @returns {(ParseObject | boolean)}
    */
-  removeAll(attr: string, items: Array): ParseObject | boolean {
+  removeAll(attr: string, items: Array): ParseObject | boolean {
     return this.set(attr, new RemoveOp(items));
   }
 
@@ -912,7 +933,7 @@ class ParseObject {
    * @param attr {String} The key.
    * @returns {Parse.Op | undefined} The operation, or undefined if none.
    */
-  op(attr: string): ?Op {
+  op(attr: string): Op | undefined {
     const pending = this._getPendingOps();
     for (let i = pending.length; i--;) {
       if (pending[i][attr]) {
@@ -926,11 +947,11 @@ class ParseObject {
    *
    * @returns {Parse.Object}
    */
-  clone(): any {
-    const clone = new this.constructor(this.className);
+  clone(): typeof this {
+    const clone = new (this.constructor as new (...args: ConstructorParameters) => this)(this.className);
     let attributes = this.attributes;
-    if (typeof this.constructor.readOnlyAttributes === 'function') {
-      const readonly = this.constructor.readOnlyAttributes() || [];
+    if (typeof (this.constructor as any).readOnlyAttributes === 'function') {
+      const readonly = (this.constructor as any).readOnlyAttributes() || [];
       // Attributes are frozen, so we have to rebuild an object,
       // rather than delete readonly keys
       const copy = {};
@@ -953,7 +974,7 @@ class ParseObject {
    * @returns {Parse.Object}
    */
   newInstance(): any {
-    const clone = new this.constructor(this.className);
+    const clone = new (this.constructor as new (...args: ConstructorParameters) => this)(this.className);
     clone.id = this.id;
     if (singleInstance) {
       // Just return an object with the right id
@@ -1060,7 +1081,7 @@ class ParseObject {
    * @returns {Parse.ACL|null} An instance of Parse.ACL.
    * @see Parse.Object#get
    */
-  getACL(): ?ParseACL {
+  getACL(): ParseACL | null {
     const acl = this.get('ACL');
     if (acl instanceof ParseACL) {
       return acl;
@@ -1076,7 +1097,7 @@ class ParseObject {
    * @returns {(ParseObject | boolean)} Whether the set passed validation.
    * @see Parse.Object#set
    */
-  setACL(acl: ParseACL, options?: mixed): ParseObject | boolean {
+  setACL(acl: ParseACL, options?: any): ParseObject | boolean {
     return this.set('ACL', acl, options);
   }
 
@@ -1109,8 +1130,8 @@ class ParseObject {
     const attributes = this.attributes;
     const erasable = {};
     let readonly = ['createdAt', 'updatedAt'];
-    if (typeof this.constructor.readOnlyAttributes === 'function') {
-      readonly = readonly.concat(this.constructor.readOnlyAttributes());
+    if (typeof (this.constructor as any).readOnlyAttributes === 'function') {
+      readonly = readonly.concat((this.constructor as any).readOnlyAttributes());
     }
     for (const attr in attributes) {
       if (readonly.indexOf(attr) < 0) {
@@ -1137,9 +1158,9 @@ class ParseObject {
    * @returns {Promise} A promise that is fulfilled when the fetch
    *     completes.
    */
-  fetch(options: RequestOptions): Promise {
+  fetch(options: ObjectFetchOptions): Promise {
     options = options || {};
-    const fetchOptions = {};
+    const fetchOptions: ObjectFetchOptions = {};
     if (options.hasOwnProperty('useMasterKey')) {
       fetchOptions.useMasterKey = options.useMasterKey;
     }
@@ -1154,13 +1175,13 @@ class ParseObject {
       if (Array.isArray(options.include)) {
         options.include.forEach(key => {
           if (Array.isArray(key)) {
-            fetchOptions.include = fetchOptions.include.concat(key);
+            fetchOptions.include = fetchOptions.include!.concat(key);
           } else {
-            fetchOptions.include.push(key);
+            (fetchOptions.include as string[]).push(key);
           }
         });
       } else {
-        fetchOptions.include.push(options.include);
+        fetchOptions.include.push(options.include!); // already checked hasOwnProperty('include')
       }
     }
     const controller = CoreManager.getObjectController();
@@ -1185,7 +1206,7 @@ class ParseObject {
    * @returns {Promise} A promise that is fulfilled when the fetch
    *     completes.
    */
-  fetchWithInclude(keys: String | Array>, options: RequestOptions): Promise {
+  fetchWithInclude(keys: String | Array>, options: RequestOptions): Promise {
     options = options || {};
     options.include = keys;
     return this.fetch(options);
@@ -1215,7 +1236,7 @@ class ParseObject {
    * @returns {Promise} A promise that is fulfilled when the save
    * completes.
    */
-  async saveEventually(options: SaveOptions): Promise {
+  async saveEventually(options: SaveOptions): Promise {
     try {
       await this.save(null, options);
     } catch (e) {
@@ -1291,14 +1312,14 @@ class ParseObject {
    * completes.
    */
   save(
-    arg1: ?string | { [attr: string]: mixed },
-    arg2: SaveOptions | mixed,
+    arg1: undefined | string | { [attr: string]: any } | null,
+    arg2: SaveOptions | any,
     arg3?: SaveOptions
-  ): Promise {
-    let attrs;
-    let options;
-    if (typeof arg1 === 'object' || typeof arg1 === 'undefined') {
-      attrs = arg1;
+  ): Promise {
+    let attrs: { [attr: string]: any } | null;
+    let options: SaveOptions | undefined;
+    if (typeof arg1 === 'object' || typeof arg1 === 'undefined' || arg1 == null) {
+      attrs = (arg1 as { [attr: string]: any }) || null;
       if (typeof arg2 === 'object') {
         options = arg2;
       }
@@ -1317,7 +1338,7 @@ class ParseObject {
     }
 
     options = options || {};
-    const saveOptions = {};
+    const saveOptions: SaveOptions = {};
     if (options.hasOwnProperty('useMasterKey')) {
       saveOptions.useMasterKey = !!options.useMasterKey;
     }
@@ -1334,7 +1355,7 @@ class ParseObject {
     const unsaved = options.cascadeSave !== false ? unsavedChildren(this) : null;
     return controller.save(unsaved, saveOptions).then(() => {
       return controller.save(this, saveOptions);
-    });
+    }) as Promise as Promise;
   }
 
   /**
@@ -1359,7 +1380,7 @@ class ParseObject {
    * @returns {Promise} A promise that is fulfilled when the destroy
    *     completes.
    */
-  async destroyEventually(options: RequestOptions): Promise {
+  async destroyEventually(options: RequestOptions): Promise {
     try {
       await this.destroy(options);
     } catch (e) {
@@ -1385,9 +1406,9 @@ class ParseObject {
    * @returns {Promise} A promise that is fulfilled when the destroy
    *     completes.
    */
-  destroy(options: RequestOptions): Promise {
+  destroy(options: RequestOptions): Promise {
     options = options || {};
-    const destroyOptions = {};
+    const destroyOptions: RequestOptions = {};
     if (options.hasOwnProperty('useMasterKey')) {
       destroyOptions.useMasterKey = options.useMasterKey;
     }
@@ -1400,7 +1421,7 @@ class ParseObject {
     if (!this.id) {
       return Promise.resolve();
     }
-    return CoreManager.getObjectController().destroy(this, destroyOptions);
+    return CoreManager.getObjectController().destroy(this, destroyOptions) as Promise;
   }
 
   /**
@@ -1552,7 +1573,7 @@ class ParseObject {
    * @returns {Parse.Object[]}
    */
   static fetchAll(list: Array, options: RequestOptions = {}) {
-    const queryOptions = {};
+    const queryOptions: RequestOptions = {};
     if (options.hasOwnProperty('useMasterKey')) {
       queryOptions.useMasterKey = options.useMasterKey;
     }
@@ -1662,10 +1683,8 @@ class ParseObject {
    * @static
    * @returns {Parse.Object[]}
    */
-  static fetchAllIfNeeded(list: Array, options) {
-    options = options || {};
-
-    const queryOptions = {};
+  static fetchAllIfNeeded(list: Array, options: ObjectFetchOptions = {}) {
+    const queryOptions: ObjectFetchOptions = {};
     if (options.hasOwnProperty('useMasterKey')) {
       queryOptions.useMasterKey = options.useMasterKey;
     }
@@ -1678,8 +1697,8 @@ class ParseObject {
     return CoreManager.getObjectController().fetch(list, false, queryOptions);
   }
 
-  static handleIncludeOptions(options) {
-    let include = [];
+  static handleIncludeOptions(options: { include?: string | string[] }) {
+    let include: string[] = [];
     if (Array.isArray(options.include)) {
       options.include.forEach(key => {
         if (Array.isArray(key)) {
@@ -1689,7 +1708,7 @@ class ParseObject {
         }
       });
     } else {
-      include.push(options.include);
+      include.push(options.include!);
     }
     return include;
   }
@@ -1740,8 +1759,8 @@ class ParseObject {
    * @returns {Promise} A promise that is fulfilled when the destroyAll
    * completes.
    */
-  static destroyAll(list: Array, options = {}) {
-    const destroyOptions = {};
+  static destroyAll(list: Array, options: SaveOptions = {}) {
+    const destroyOptions: SaveOptions = {};
     if (options.hasOwnProperty('useMasterKey')) {
       destroyOptions.useMasterKey = options.useMasterKey;
     }
@@ -1775,8 +1794,8 @@ class ParseObject {
    * @static
    * @returns {Parse.Object[]}
    */
-  static saveAll(list: Array, options: RequestOptions = {}) {
-    const saveOptions = {};
+  static saveAll(list: Array, options: SaveOptions = {}) {
+    const saveOptions: SaveOptions = {};
     if (options.hasOwnProperty('useMasterKey')) {
       saveOptions.useMasterKey = options.useMasterKey;
     }
@@ -1806,7 +1825,7 @@ class ParseObject {
    * @static
    * @returns {Parse.Object} A Parse.Object reference.
    */
-  static createWithoutData(id: string) {
+  static createWithoutData(id: string): ParseObject {
     const obj = new this();
     obj.id = id;
     return obj;
@@ -1822,13 +1841,13 @@ class ParseObject {
    * @static
    * @returns {Parse.Object} A Parse.Object reference
    */
-  static fromJSON(json: any, override?: boolean, dirty?: boolean) {
+  static fromJSON(json: any, override?: boolean, dirty?: boolean): ParseObject {
     if (!json.className) {
       throw new Error('Cannot create an object without a className');
     }
     const constructor = classMap[json.className];
     const o = constructor ? new constructor(json.className) : new ParseObject(json.className);
-    const otherAttributes = {};
+    const otherAttributes: AttributeMap = {};
     for (const attr in json) {
       if (attr !== 'className' && attr !== '__type') {
         otherAttributes[attr] = json[attr];
@@ -1877,7 +1896,7 @@ class ParseObject {
     if (typeof constructor !== 'function') {
       throw new TypeError(
         'You must register the subclass constructor. ' +
-          'Did you attempt to register an instance of the subclass?'
+        'Did you attempt to register an instance of the subclass?'
       );
     }
     classMap[className] = constructor;
@@ -1935,7 +1954,7 @@ class ParseObject {
    *     this method.
    * @returns {Parse.Object} A new subclass of Parse.Object.
    */
-  static extend(className: any, protoProps: any, classProps: any) {
+  static extend(className: any, protoProps?: any, classProps?: any) {
     if (typeof className !== 'string') {
       if (className && typeof className.className === 'string') {
         return ParseObject.extend(className.className, className, protoProps);
@@ -1950,7 +1969,7 @@ class ParseObject {
     }
 
     let parentProto = ParseObject.prototype;
-    if (this.hasOwnProperty('__super__') && this.__super__) {
+    if (this.hasOwnProperty('__super__') && (this as any).__super__) {
       parentProto = this.prototype;
     }
     let ParseObjectSubclass = function (attributes, options) {
@@ -1976,15 +1995,16 @@ class ParseObject {
     if (classMap[adjustedClassName]) {
       ParseObjectSubclass = classMap[adjustedClassName];
     } else {
-      ParseObjectSubclass.extend = function (name, protoProps, classProps) {
+      // TODO: Maybe there is a more elegant solution to this?
+      (ParseObjectSubclass as any).extend = function (name: string, protoProps: any, classProps: any) {
         if (typeof name === 'string') {
           return ParseObject.extend.call(ParseObjectSubclass, name, protoProps, classProps);
         }
         return ParseObject.extend.call(ParseObjectSubclass, adjustedClassName, name, protoProps);
       };
-      ParseObjectSubclass.createWithoutData = ParseObject.createWithoutData;
-      ParseObjectSubclass.className = adjustedClassName;
-      ParseObjectSubclass.__super__ = parentProto;
+      (ParseObjectSubclass as any).createWithoutData = ParseObject.createWithoutData;
+      (ParseObjectSubclass as any).className = adjustedClassName;
+      (ParseObjectSubclass as any).__super__ = parentProto;
       ParseObjectSubclass.prototype = Object.create(parentProto, {
         constructor: {
           value: ParseObjectSubclass,
@@ -2195,17 +2215,19 @@ const DefaultController = {
     target: ParseObject | Array,
     forceFetch: boolean,
     options: RequestOptions
-  ): Promise | ParseObject> {
+  ): Promise | ParseObject | undefined> {
     const localDatastore = CoreManager.getLocalDatastore();
     if (Array.isArray(target)) {
       if (target.length < 1) {
         return Promise.resolve([]);
       }
-      const objs = [];
-      const ids = [];
-      let className = null;
-      const results = [];
-      let error = null;
+      /** Resulting Parse.Objects that have data */
+      const objs: ParseObject[] = [];
+      /** IDs to fetch */
+      const ids: string[] = [];
+      let className: null | string = null;
+      const results: ParseObject[] = [];
+      let error: ParseError | null = null;
       target.forEach(el => {
         if (error) {
           return;
@@ -2223,7 +2245,7 @@ const DefaultController = {
           error = new ParseError(ParseError.MISSING_OBJECT_ID, 'All objects must have an ID');
         }
         if (forceFetch || !el.isDataAvailable()) {
-          ids.push(el.id);
+          ids.push(el.id!); // Already checked e.id above.
           objs.push(el);
         }
         results.push(el);
@@ -2231,16 +2253,17 @@ const DefaultController = {
       if (error) {
         return Promise.reject(error);
       }
-      const query = new ParseQuery(className);
+      // Construct a ParseQuery that finds objects with matching IDs
+      const query = new ParseQuery(className!);
       query.containedIn('objectId', ids);
       if (options && options.include) {
         query.include(options.include);
       }
       query._limit = ids.length;
-      return query.find(options).then(async objects => {
-        const idMap = {};
+      return query.find(options).then(async (objects: ParseObject[]) => {
+        const idMap: Record = {};
         objects.forEach(o => {
-          idMap[o.id] = o;
+          idMap[o.id!] = o;
         });
         for (let i = 0; i < objs.length; i++) {
           const obj = objs[i];
@@ -2253,7 +2276,7 @@ const DefaultController = {
           }
         }
         if (!singleInstance) {
-          // If single instance objects are disabled, we need to replace the
+          // If single instance objects are disabled, we need to replace the objects in the results array.
           for (let i = 0; i < results.length; i++) {
             const obj = results[i];
             if (obj && obj.id && idMap[obj.id]) {
@@ -2275,7 +2298,7 @@ const DefaultController = {
         );
       }
       const RESTController = CoreManager.getRESTController();
-      const params = {};
+      const params: RequestOptions = {};
       if (options && options.include) {
         params.include = options.include.join();
       }
@@ -2292,13 +2315,14 @@ const DefaultController = {
         return target;
       });
     }
-    return Promise.resolve();
+    // Not Array, and not ParseObject; return undefined/void.
+    return Promise.resolve(undefined);
   },
 
   async destroy(
     target: ParseObject | Array,
-    options: RequestOptions
-  ): Promise | ParseObject> {
+    options: SaveOptions
+  ): Promise | ParseObject> {
     const batchSize =
       options && options.batchSize ? options.batchSize : CoreManager.get('REQUEST_BATCH_SIZE');
     const localDatastore = CoreManager.getLocalDatastore();
@@ -2308,7 +2332,7 @@ const DefaultController = {
       if (target.length < 1) {
         return Promise.resolve([]);
       }
-      const batches = [[]];
+      const batches: ParseObject[][] = [[]];
       target.forEach(obj => {
         if (!obj.id) {
           return;
@@ -2323,7 +2347,7 @@ const DefaultController = {
         batches.pop();
       }
       let deleteCompleted = Promise.resolve();
-      const errors = [];
+      const errors: ParseError[] = [];
       batches.forEach(batch => {
         deleteCompleted = deleteCompleted.then(() => {
           return RESTController.request(
@@ -2375,7 +2399,7 @@ const DefaultController = {
     return Promise.resolve(target);
   },
 
-  save(target: ParseObject | Array, options: RequestOptions) {
+  save(target: ParseObject | Array | null, options: RequestOptions) {
     const batchSize =
       options && options.batchSize ? options.batchSize : CoreManager.get('REQUEST_BATCH_SIZE');
     const localDatastore = CoreManager.getLocalDatastore();
@@ -2394,13 +2418,14 @@ const DefaultController = {
 
       let unsaved = target.concat();
       for (let i = 0; i < target.length; i++) {
-        if (target[i] instanceof ParseObject) {
-          unsaved = unsaved.concat(unsavedChildren(target[i], true));
+        const target_i = target[i];
+        if (target_i instanceof ParseObject) {
+          unsaved = unsaved.concat(unsavedChildren(target_i, true));
         }
       }
       unsaved = unique(unsaved);
 
-      const filesSaved: Array = [];
+      const filesSaved: Array | undefined> = [];
       let pending: Array = [];
       unsaved.forEach(el => {
         if (el instanceof ParseFile) {
@@ -2411,14 +2436,14 @@ const DefaultController = {
       });
 
       return Promise.all(filesSaved).then(() => {
-        let objectError = null;
+        let objectError: null | ParseError = null;
         return continueWhile(
           () => {
             return pending.length > 0;
           },
           () => {
-            const batch = [];
-            const nextPending = [];
+            const batch: ParseObject[] = [];
+            const nextPending: ParseObject[] = [];
             pending.forEach(el => {
               if (allowCustomObjectId && Object.prototype.hasOwnProperty.call(el, 'id') && !el.id) {
                 throw new ParseError(
@@ -2442,11 +2467,11 @@ const DefaultController = {
 
             // Queue up tasks for each object in the batch.
             // When every task is ready, the API request will execute
-            const batchReturned = new resolvingPromise();
-            const batchReady = [];
-            const batchTasks = [];
+            const batchReturned = resolvingPromise();
+            const batchReady: ReturnType>[] = [];
+            const batchTasks: Promise[] = [];
             batch.forEach((obj, index) => {
-              const ready = new resolvingPromise();
+              const ready = resolvingPromise();
               batchReady.push(ready);
               const task = function () {
                 ready.resolve();
@@ -2503,7 +2528,7 @@ const DefaultController = {
           for (const object of target) {
             // Make sure that it is a ParseObject before updating it into the localDataStore
             if (object instanceof ParseObject) {
-              await localDatastore._updateLocalIdForObject(mapIdForPin[object.id], object);
+              await localDatastore._updateLocalIdForObject(mapIdForPin[object.id!], object);
               await localDatastore._updateObjectIfPinned(object);
             }
           }
diff --git a/src/ParseOp.js b/src/ParseOp.ts
similarity index 76%
rename from src/ParseOp.js
rename to src/ParseOp.ts
index a35485c60..5da8cde7f 100644
--- a/src/ParseOp.js
+++ b/src/ParseOp.ts
@@ -5,71 +5,73 @@
 import arrayContainsObject from './arrayContainsObject';
 import decode from './decode';
 import encode from './encode';
-import ParseObject from './ParseObject';
+import ParseObject, { Pointer } from './ParseObject';
 import ParseRelation from './ParseRelation';
 import unique from './unique';
 
-export function opFromJSON(json: { [key: string]: any }): ?Op {
+export function opFromJSON(json: { [key: string]: any }): Op | null {
   if (!json || !json.__op) {
     return null;
   }
   switch (json.__op) {
-  case 'Delete':
-    return new UnsetOp();
-  case 'Increment':
-    return new IncrementOp(json.amount);
-  case 'Add':
-    return new AddOp(decode(json.objects));
-  case 'AddUnique':
-    return new AddUniqueOp(decode(json.objects));
-  case 'Remove':
-    return new RemoveOp(decode(json.objects));
-  case 'AddRelation': {
-    const toAdd = decode(json.objects);
-    if (!Array.isArray(toAdd)) {
-      return new RelationOp([], []);
-    }
-    return new RelationOp(toAdd, []);
-  }
-  case 'RemoveRelation': {
-    const toRemove = decode(json.objects);
-    if (!Array.isArray(toRemove)) {
-      return new RelationOp([], []);
+    case 'Delete':
+      return new UnsetOp();
+    case 'Increment':
+      return new IncrementOp(json.amount);
+    case 'Add':
+      return new AddOp(decode(json.objects));
+    case 'AddUnique':
+      return new AddUniqueOp(decode(json.objects));
+    case 'Remove':
+      return new RemoveOp(decode(json.objects));
+    case 'AddRelation': {
+      const toAdd = decode(json.objects);
+      if (!Array.isArray(toAdd)) {
+        return new RelationOp([], []);
+      }
+      return new RelationOp(toAdd, []);
     }
-    return new RelationOp([], toRemove);
-  }
-  case 'Batch': {
-    let toAdd = [];
-    let toRemove = [];
-    for (let i = 0; i < json.ops.length; i++) {
-      if (json.ops[i].__op === 'AddRelation') {
-        toAdd = toAdd.concat(decode(json.ops[i].objects));
-      } else if (json.ops[i].__op === 'RemoveRelation') {
-        toRemove = toRemove.concat(decode(json.ops[i].objects));
+    case 'RemoveRelation': {
+      const toRemove = decode(json.objects);
+      if (!Array.isArray(toRemove)) {
+        return new RelationOp([], []);
       }
+      return new RelationOp([], toRemove);
+    }
+    case 'Batch': {
+      let toAdd = [];
+      let toRemove = [];
+      for (let i = 0; i < json.ops.length; i++) {
+        if (json.ops[i].__op === 'AddRelation') {
+          toAdd = toAdd.concat(decode(json.ops[i].objects));
+        } else if (json.ops[i].__op === 'RemoveRelation') {
+          toRemove = toRemove.concat(decode(json.ops[i].objects));
+        }
+      }
+      return new RelationOp(toAdd, toRemove);
     }
-    return new RelationOp(toAdd, toRemove);
-  }
   }
   return null;
 }
 
 export class Op {
+  __op?: string;
+  objects?: any[];
   // Empty parent class
-  applyTo(value: mixed): mixed {} /* eslint-disable-line no-unused-vars */
-  mergeWith(previous: Op): ?Op {} /* eslint-disable-line no-unused-vars */
-  toJSON(): mixed {}
+  applyTo(...value: any): any { } /* eslint-disable-line no-unused-vars */
+  mergeWith(previous: Op): Op | void { } /* eslint-disable-line no-unused-vars */
+  toJSON(offline?: boolean): any { }
 }
 
 export class SetOp extends Op {
-  _value: ?mixed;
+  _value: any;
 
-  constructor(value: mixed) {
+  constructor(value: any) {
     super();
     this._value = value;
   }
 
-  applyTo(): mixed {
+  applyTo(): any {
     return this._value;
   }
 
@@ -77,7 +79,7 @@ export class SetOp extends Op {
     return new SetOp(this._value);
   }
 
-  toJSON(offline?: boolean) {
+  toJSON(offline?: boolean) : any {
     return encode(this._value, false, true, undefined, offline);
   }
 }
@@ -107,7 +109,7 @@ export class IncrementOp extends Op {
     this._amount = amount;
   }
 
-  applyTo(value: ?mixed): number {
+  applyTo(value: any): number {
     if (typeof value === 'undefined') {
       return this._amount;
     }
@@ -139,14 +141,14 @@ export class IncrementOp extends Op {
 }
 
 export class AddOp extends Op {
-  _value: Array;
+  _value: Array;
 
-  constructor(value: mixed | Array) {
+  constructor(value: any | Array) {
     super();
     this._value = Array.isArray(value) ? value : [value];
   }
 
-  applyTo(value: mixed): Array {
+  applyTo(value: any): Array {
     if (value == null) {
       return this._value;
     }
@@ -172,25 +174,25 @@ export class AddOp extends Op {
     throw new Error('Cannot merge Add Op with the previous Op');
   }
 
-  toJSON(): { __op: string, objects: mixed } {
+  toJSON(): { __op: string, objects: any } {
     return { __op: 'Add', objects: encode(this._value, false, true) };
   }
 }
 
 export class AddUniqueOp extends Op {
-  _value: Array;
+  _value: Array;
 
-  constructor(value: mixed | Array) {
+  constructor(value: any | Array) {
     super();
     this._value = unique(Array.isArray(value) ? value : [value]);
   }
 
-  applyTo(value: mixed | Array): Array {
+  applyTo(value: any | Array): Array {
     if (value == null) {
       return this._value || [];
     }
     if (Array.isArray(value)) {
-      const toAdd = [];
+      const toAdd: any[] = [];
       this._value.forEach(v => {
         if (v instanceof ParseObject) {
           if (!arrayContainsObject(value, v)) {
@@ -223,20 +225,20 @@ export class AddUniqueOp extends Op {
     throw new Error('Cannot merge AddUnique Op with the previous Op');
   }
 
-  toJSON(): { __op: string, objects: mixed } {
+  toJSON(): { __op: string, objects: any } {
     return { __op: 'AddUnique', objects: encode(this._value, false, true) };
   }
 }
 
 export class RemoveOp extends Op {
-  _value: Array;
+  _value: Array;
 
-  constructor(value: mixed | Array) {
+  constructor(value: any | Array) {
     super();
     this._value = unique(Array.isArray(value) ? value : [value]);
   }
 
-  applyTo(value: mixed | Array): Array {
+  applyTo(value: any | Array): Array {
     if (value == null) {
       return [];
     }
@@ -291,13 +293,13 @@ export class RemoveOp extends Op {
     throw new Error('Cannot merge Remove Op with the previous Op');
   }
 
-  toJSON(): { __op: string, objects: mixed } {
+  toJSON(): { __op: string, objects: any } {
     return { __op: 'Remove', objects: encode(this._value, false, true) };
   }
 }
 
 export class RelationOp extends Op {
-  _targetClassName: ?string;
+  _targetClassName: string | null;
   relationsToAdd: Array;
   relationsToRemove: Array;
 
@@ -327,16 +329,16 @@ export class RelationOp extends Op {
     if (this._targetClassName !== obj.className) {
       throw new Error(
         'Tried to create a Relation with 2 different object types: ' +
-          this._targetClassName +
-          ' and ' +
-          obj.className +
-          '.'
+        this._targetClassName +
+        ' and ' +
+        obj.className +
+        '.'
       );
     }
     return obj.id;
   }
 
-  applyTo(value: mixed, object?: { className: string, id: ?string }, key?: string): ?ParseRelation {
+  applyTo(value: any, object?: { className: string, id?: string }, key?: string): ParseRelation {
     if (!value) {
       if (!object || !key) {
         throw new Error(
@@ -359,10 +361,10 @@ export class RelationOp extends Op {
           if (this._targetClassName !== value.targetClassName) {
             throw new Error(
               'Related object must be a ' +
-                value.targetClassName +
-                ', but a ' +
-                this._targetClassName +
-                ' was passed in.'
+              value.targetClassName +
+              ', but a ' +
+              this._targetClassName +
+              ' was passed in.'
             );
           }
         } else {
@@ -386,10 +388,10 @@ export class RelationOp extends Op {
       if (previous._targetClassName && previous._targetClassName !== this._targetClassName) {
         throw new Error(
           'Related object must be of class ' +
-            previous._targetClassName +
-            ', but ' +
-            (this._targetClassName || 'null') +
-            ' was passed in.'
+          previous._targetClassName +
+          ', but ' +
+          (this._targetClassName || 'null') +
+          ' was passed in.'
         );
       }
       const newAdd = previous.relationsToAdd.concat([]);
@@ -427,18 +429,19 @@ export class RelationOp extends Op {
     throw new Error('Cannot merge Relation Op with the previous Op');
   }
 
-  toJSON(): { __op?: string, objects?: mixed, ops?: mixed } {
-    const idToPointer = id => {
-      return {
+  toJSON(): { __op?: string, objects?: any, ops?: any } {
+    const idToPointer = (id: string) => {
+      const ret: Pointer = {
         __type: 'Pointer',
-        className: this._targetClassName,
+        className: this._targetClassName!,
         objectId: id,
       };
+      return ret;
     };
 
-    let adds = null;
-    let removes = null;
-    let pointers = null;
+    let pointers: null | (Pointer[]) = null;
+    let adds: null | { __op: string, objects: null | (Pointer[]) } = null;
+    let removes: null | { __op: string, objects: null | (Pointer[]) } = null;
 
     if (this.relationsToAdd.length > 0) {
       pointers = this.relationsToAdd.map(idToPointer);
@@ -452,7 +455,6 @@ export class RelationOp extends Op {
     if (adds && removes) {
       return { __op: 'Batch', ops: [adds, removes] };
     }
-
     return adds || removes || {};
   }
 }
diff --git a/src/ParsePolygon.js b/src/ParsePolygon.ts
similarity index 94%
rename from src/ParsePolygon.js
rename to src/ParsePolygon.ts
index a7887ea02..27a8ed367 100644
--- a/src/ParsePolygon.js
+++ b/src/ParsePolygon.ts
@@ -24,12 +24,12 @@ import ParseGeoPoint from './ParseGeoPoint';
  * @alias Parse.Polygon
  */
 class ParsePolygon {
-  _coordinates: Array>;
+  _coordinates: Array<[number, number]>;
 
   /**
    * @param {(number[][] | Parse.GeoPoint[])} coordinates An Array of coordinate pairs
    */
-  constructor(coordinates: Array> | Array) {
+  constructor(coordinates: Array | [number, number]> | Array) {
     this._coordinates = ParsePolygon._validate(coordinates);
   }
 
@@ -67,7 +67,7 @@ class ParsePolygon {
    * @param {(Parse.Polygon | object)} other
    * @returns {boolean}
    */
-  equals(other: mixed): boolean {
+  equals(other: ParsePolygon | any): boolean {
     if (!(other instanceof ParsePolygon) || this.coordinates.length !== other.coordinates.length) {
       return false;
     }
@@ -138,14 +138,14 @@ class ParsePolygon {
    * @throws {TypeError}
    * @returns {number[][]} Array of coordinates if validated.
    */
-  static _validate(coords: Array> | Array): Array> {
+  static _validate(coords: Array> | Array): Array<[number, number]> {
     if (!Array.isArray(coords)) {
       throw new TypeError('Coordinates must be an Array');
     }
     if (coords.length < 3) {
       throw new TypeError('Polygon must have at least 3 GeoPoints or Points');
     }
-    const points = [];
+    const points: [number, number][] = [];
     for (let i = 0; i < coords.length; i += 1) {
       const coord = coords[i];
       let geoPoint;
diff --git a/src/ParseQuery.js b/src/ParseQuery.ts
similarity index 92%
rename from src/ParseQuery.js
rename to src/ParseQuery.ts
index 9be6b37a9..70a8d9adb 100644
--- a/src/ParseQuery.js
+++ b/src/ParseQuery.ts
@@ -14,10 +14,47 @@ import { DEFAULT_PIN } from './LocalDatastoreUtils';
 import type LiveQuerySubscription from './LiveQuerySubscription';
 import type { RequestOptions, FullOptions } from './RESTController';
 
-type BatchOptions = FullOptions & { batchSize?: number };
+/**
+ *    *   
  • batchSize: How many objects to yield in each batch (default: 100) + *
  • useMasterKey: In Cloud Code and Node only, causes the Master Key to + * be used for this request. + *
  • sessionToken: A valid session token, used for making a request on + * behalf of a specific user. + *
  • context: A dictionary that is accessible in Cloud Code `beforeFind` trigger. + * + */ +type BatchOptions = FullOptions & { + batchSize?: number + useMasterKey?: boolean, + sessionToken?: string, + context?: { [key: string]: any }, + json?: boolean +}; + +/** + * Valid options are: + * useMasterKey: In Cloud Code and Node only, causes the Master Key to be used for this request. + * sessionToken: A valid session token, used for making a request on behalf of a specific user. + * context: A dictionary that is accessible in Cloud Code `beforeFind` trigger. + * json: Return raw json without converting to Parse.Object + */ +type QueryOptions = { + useMasterKey?: boolean, + sessionToken?: string, + context?: { [key: string]: any }, + json?: boolean +} +type FullTextQueryOptions = { + /** @param {string} options.language The language that determines the list of stop words for the search and the rules for the stemmer and tokenizer.*/ + language?: string, + /** @param {boolean} options.caseSensitive A boolean flag to enable or disable case sensitive search.*/ + caseSensitive?: boolean, + /** @param {boolean} options.diacriticSensitive A boolean flag to enable or disable diacritic sensitive search.*/ + diacriticSensitive?: boolean +} export type WhereClause = { - [attr: string]: mixed, + [attr: string]: any, }; export type QueryJSON = { @@ -31,7 +68,7 @@ export type QueryJSON = { order?: string, className?: string, count?: number, - hint?: mixed, + hint?: any, explain?: boolean, readPreference?: string, includeReadPreference?: string, @@ -59,8 +96,8 @@ function quote(s: string): string { * @private * @returns {string} */ -function _getClassNameFromQueries(queries: Array): ?string { - let className = null; +function _getClassNameFromQueries(queries: Array): string | null { + let className: string | null = null; queries.forEach(q => { if (!className) { className = q.className; @@ -233,20 +270,20 @@ class ParseQuery { _skip: number; _count: boolean; _order: Array; - _readPreference: string; - _includeReadPreference: string; - _subqueryReadPreference: string; + _readPreference: string | null; + _includeReadPreference: string | null; + _subqueryReadPreference: string | null; _queriesLocalDatastore: boolean; _localDatastorePinName: any; - _extraOptions: { [key: string]: mixed }; - _hint: mixed; + _extraOptions: { [key: string]: any }; + _hint: any; _explain: boolean; _xhrRequest: any; /** * @param {(string | Parse.Object)} objectClass An instance of a subclass of Parse.Object, or a Parse className string. */ - constructor(objectClass: string | ParseObject) { + constructor(objectClass: string | ParseObject | (typeof ParseObject)) { if (typeof objectClass === 'string') { if (objectClass === 'User' && CoreManager.get('PERFORM_USER_REWRITE')) { this.className = '_User'; @@ -256,10 +293,11 @@ class ParseQuery { } else if (objectClass instanceof ParseObject) { this.className = objectClass.className; } else if (typeof objectClass === 'function') { - if (typeof objectClass.className === 'string') { - this.className = objectClass.className; + const objectClz = (objectClass) as typeof ParseObject; + if (typeof objectClz.className === 'string') { + this.className = objectClz.className; } else { - const obj = new objectClass(); + const obj = new objectClz(); this.className = obj.className; } } else { @@ -281,7 +319,7 @@ class ParseQuery { this._extraOptions = {}; this._xhrRequest = { task: null, - onchange: () => {}, + onchange: () => { }, }; } @@ -338,7 +376,7 @@ class ParseQuery { * @param value * @returns {Parse.Query} */ - _addCondition(key: string, condition: string, value: mixed): ParseQuery { + _addCondition(key: string, condition: string, value: any): ParseQuery { if (!this._where[key] || typeof this._where[key] === 'string') { this._where[key] = {}; } @@ -356,7 +394,7 @@ class ParseQuery { return '^' + quote(string); } - async _handleOfflineQuery(params: any) { + async _handleOfflineQuery(params: QueryJSON) { OfflineQuery.validateQuery(this); const localDatastore = CoreManager.getLocalDatastore(); const objects = await localDatastore._serializeObjectsFromPinName(this._localDatastorePinName); @@ -609,10 +647,10 @@ class ParseQuery { * @returns {Promise} A promise that is resolved with the result when * the query completes. */ - get(objectId: string, options?: FullOptions): Promise { + get(objectId: string, options?: QueryOptions): Promise { this.equalTo('objectId', objectId); - const firstOptions = {}; + const firstOptions: QueryOptions = {}; if (options && options.hasOwnProperty('useMasterKey')) { firstOptions.useMasterKey = options.useMasterKey; } @@ -630,7 +668,6 @@ class ParseQuery { if (response) { return response; } - const errorObject = new ParseError(ParseError.OBJECT_NOT_FOUND, 'Object not found.'); return Promise.reject(errorObject); }); @@ -651,10 +688,10 @@ class ParseQuery { * @returns {Promise} A promise that is resolved with the results when * the query completes. */ - find(options?: FullOptions): Promise> { + find(options?: QueryOptions): Promise> { options = options || {}; - const findOptions = {}; + const findOptions: QueryOptions = {}; if (options.hasOwnProperty('useMasterKey')) { findOptions.useMasterKey = options.useMasterKey; } @@ -678,7 +715,7 @@ class ParseQuery { if (this._explain) { return response.results; } - const results = response.results.map(data => { + const results = response.results!.map(data => { // In cases of relations, the server may send back a className // on the top level of the payload const override = response.className || this.className; @@ -692,7 +729,7 @@ class ParseQuery { if (select) { handleSelectResult(data, select); } - if (options.json) { + if (options?.json) { return data; } else { return ParseObject.fromJSON(data, !select); @@ -702,7 +739,7 @@ class ParseQuery { const count = response.count; if (typeof count === 'number') { - return { results, count }; + return { results, count } as any; } else { return results; } @@ -744,10 +781,10 @@ class ParseQuery { * @returns {Promise} A promise that is resolved with the count when * the query completes. */ - count(options?: FullOptions): Promise { + count(options?: { useMasterKey?: boolean, sessionToken?: string }): Promise { options = options || {}; - const findOptions = {}; + const findOptions: { useMasterKey?: boolean, sessionToken?: string } = {}; if (options.hasOwnProperty('useMasterKey')) { findOptions.useMasterKey = options.useMasterKey; } @@ -763,7 +800,7 @@ class ParseQuery { params.count = 1; return controller.find(this.className, params, findOptions).then(result => { - return result.count; + return result.count!; }); } @@ -778,12 +815,10 @@ class ParseQuery { * * @returns {Promise} A promise that is resolved with the query completes. */ - distinct(key: string, options?: FullOptions): Promise> { + distinct(key: string, options?: { sessionToken?: string }): Promise> { options = options || {}; - const distinctOptions = {}; - distinctOptions.useMasterKey = true; - + const distinctOptions: { sessionToken?: string, useMasterKey: boolean } = { useMasterKey: true }; if (options.hasOwnProperty('sessionToken')) { distinctOptions.sessionToken = options.sessionToken; } @@ -796,7 +831,7 @@ class ParseQuery { hint: this._hint, }; return controller.aggregate(this.className, params, distinctOptions).then(results => { - return results.results; + return results.results!; }); } @@ -810,10 +845,9 @@ class ParseQuery { * * @returns {Promise} A promise that is resolved with the query completes. */ - aggregate(pipeline: mixed, options?: FullOptions): Promise> { + aggregate(pipeline: any, options?: { sessionToken?: string }): Promise> { options = options || {}; - const aggregateOptions = {}; - aggregateOptions.useMasterKey = true; + const aggregateOptions: { sessionToken?: string, useMasterKey: boolean } = { useMasterKey: true }; if (options.hasOwnProperty('sessionToken')) { aggregateOptions.sessionToken = options.sessionToken; @@ -840,7 +874,7 @@ class ParseQuery { readPreference: this._readPreference, }; return controller.aggregate(this.className, params, aggregateOptions).then(results => { - return results.results; + return results.results!; }); } @@ -860,10 +894,8 @@ class ParseQuery { * @returns {Promise} A promise that is resolved with the object when * the query completes. */ - first(options?: FullOptions): Promise { - options = options || {}; - - const findOptions = {}; + first(options: QueryOptions = {}): Promise { + const findOptions: QueryOptions = {}; if (options.hasOwnProperty('useMasterKey')) { findOptions.useMasterKey = options.useMasterKey; } @@ -892,7 +924,7 @@ class ParseQuery { } return controller.find(this.className, params, findOptions).then(response => { - const objects = response.results; + const objects = response.results!; if (!objects[0]) { return undefined; } @@ -936,7 +968,7 @@ class ParseQuery { * iteration has completed. */ eachBatch( - callback: (objs: Array) => Promise<*>, + callback: (objs: Array) => void, options?: BatchOptions ): Promise { options = options || {}; @@ -974,7 +1006,7 @@ class ParseQuery { query.ascending('objectId'); - const findOptions = {}; + const findOptions: BatchOptions = {}; if (options.hasOwnProperty('useMasterKey')) { findOptions.useMasterKey = options.useMasterKey; } @@ -989,7 +1021,7 @@ class ParseQuery { } let finished = false; - let previousResults = []; + let previousResults: ParseObject[] = []; return continueWhile( () => { return !finished; @@ -1050,7 +1082,7 @@ class ParseQuery { * @param {(string|object)} value String or Object of index that should be used when executing query * @returns {Parse.Query} Returns the query, so you can chain this call. */ - hint(value: mixed): ParseQuery { + hint(value: any): ParseQuery { if (typeof value === 'undefined') { delete this._hint; } @@ -1098,7 +1130,7 @@ class ParseQuery { callback: (currentObject: ParseObject, index: number, query: ParseQuery) => any, options?: BatchOptions ): Promise> { - const array = []; + const array: any[] = []; let index = 0; await this.each(object => { return Promise.resolve(callback(object, index, this)).then(result => { @@ -1186,7 +1218,7 @@ class ParseQuery { callback: (currentObject: ParseObject, index: number, query: ParseQuery) => boolean, options?: BatchOptions ): Promise> { - const array = []; + const array: ParseObject[] = []; let index = 0; await this.each(object => { return Promise.resolve(callback(object, index, this)).then(flag => { @@ -1209,16 +1241,16 @@ class ParseQuery { * @param value The value that the Parse.Object must contain. * @returns {Parse.Query} Returns the query, so you can chain this call. */ - equalTo(key: string | { [key: string]: any }, value: ?mixed): ParseQuery { + equalTo(key: string | { [key: string]: any }, value?: any): ParseQuery { if (key && typeof key === 'object') { Object.entries(key).forEach(([k, val]) => this.equalTo(k, val)); return this; } if (typeof value === 'undefined') { - return this.doesNotExist(key); + return this.doesNotExist(key as string); } - this._where[key] = encode(value, false, true); + this._where[key as string] = encode(value, false, true); return this; } @@ -1230,12 +1262,12 @@ class ParseQuery { * @param value The value that must not be equalled. * @returns {Parse.Query} Returns the query, so you can chain this call. */ - notEqualTo(key: string | { [key: string]: any }, value: ?mixed): ParseQuery { + notEqualTo(key: string | { [key: string]: any }, value?: any): ParseQuery { if (key && typeof key === 'object') { Object.entries(key).forEach(([k, val]) => this.notEqualTo(k, val)); return this; } - return this._addCondition(key, '$ne', value); + return this._addCondition(key as string, '$ne', value); } /** @@ -1246,7 +1278,7 @@ class ParseQuery { * @param value The value that provides an upper bound. * @returns {Parse.Query} Returns the query, so you can chain this call. */ - lessThan(key: string, value: mixed): ParseQuery { + lessThan(key: string, value: any): ParseQuery { return this._addCondition(key, '$lt', value); } @@ -1258,7 +1290,7 @@ class ParseQuery { * @param value The value that provides an lower bound. * @returns {Parse.Query} Returns the query, so you can chain this call. */ - greaterThan(key: string, value: mixed): ParseQuery { + greaterThan(key: string, value: any): ParseQuery { return this._addCondition(key, '$gt', value); } @@ -1270,7 +1302,7 @@ class ParseQuery { * @param value The value that provides an upper bound. * @returns {Parse.Query} Returns the query, so you can chain this call. */ - lessThanOrEqualTo(key: string, value: mixed): ParseQuery { + lessThanOrEqualTo(key: string, value: any): ParseQuery { return this._addCondition(key, '$lte', value); } @@ -1282,7 +1314,7 @@ class ParseQuery { * @param {*} value The value that provides an lower bound. * @returns {Parse.Query} Returns the query, so you can chain this call. */ - greaterThanOrEqualTo(key: string, value: mixed): ParseQuery { + greaterThanOrEqualTo(key: string, value: any): ParseQuery { return this._addCondition(key, '$gte', value); } @@ -1294,7 +1326,7 @@ class ParseQuery { * @param {Array<*>} value The values that will match. * @returns {Parse.Query} Returns the query, so you can chain this call. */ - containedIn(key: string, value: Array): ParseQuery { + containedIn(key: string, value: Array): ParseQuery { return this._addCondition(key, '$in', value); } @@ -1306,7 +1338,7 @@ class ParseQuery { * @param {Array<*>} value The values that will not match. * @returns {Parse.Query} Returns the query, so you can chain this call. */ - notContainedIn(key: string, value: Array): ParseQuery { + notContainedIn(key: string, value: Array): ParseQuery { return this._addCondition(key, '$nin', value); } @@ -1318,7 +1350,7 @@ class ParseQuery { * @param {Array} values The values that will match. * @returns {Parse.Query} Returns the query, so you can chain this call. */ - containedBy(key: string, values: Array): ParseQuery { + containedBy(key: string, values: Array): ParseQuery { return this._addCondition(key, '$containedBy', values); } @@ -1330,7 +1362,7 @@ class ParseQuery { * @param {Array} values The values that will match. * @returns {Parse.Query} Returns the query, so you can chain this call. */ - containsAll(key: string, values: Array): ParseQuery { + containsAll(key: string, values: Array): ParseQuery { return this._addCondition(key, '$all', values); } @@ -1385,16 +1417,18 @@ class ParseQuery { * @param {string} modifiers The regular expression mode. * @returns {Parse.Query} Returns the query, so you can chain this call. */ - matches(key: string, regex: RegExp, modifiers: string): ParseQuery { + matches(key: string, regex: RegExp | string, modifiers: string): ParseQuery { this._addCondition(key, '$regex', regex); if (!modifiers) { modifiers = ''; } - if (regex.ignoreCase) { - modifiers += 'i'; - } - if (regex.multiline) { - modifiers += 'm'; + if (typeof regex != 'string') { + if (regex.ignoreCase) { + modifiers += 'i'; + } + if (regex.multiline) { + modifiers += 'm'; + } } if (modifiers.length) { this._addCondition(key, '$options', modifiers); @@ -1516,8 +1550,7 @@ class ParseQuery { * @param {boolean} options.diacriticSensitive A boolean flag to enable or disable diacritic sensitive search. * @returns {Parse.Query} Returns the query, so you can chain this call. */ - fullText(key: string, value: string, options: ?Object): ParseQuery { - options = options || {}; + fullText(key: string, value: string, options: FullTextQueryOptions = {}): ParseQuery { if (!key) { throw new Error('A key is required.'); @@ -1529,22 +1562,22 @@ class ParseQuery { throw new Error('The value being searched for must be a string.'); } - const fullOptions = {}; + const fullOptions: { $term?: string, $language?: string, $caseSensitive?: boolean, $diacriticSensitive?: boolean } = {}; fullOptions.$term = value; for (const option in options) { switch (option) { - case 'language': - fullOptions.$language = options[option]; - break; - case 'caseSensitive': - fullOptions.$caseSensitive = options[option]; - break; - case 'diacriticSensitive': - fullOptions.$diacriticSensitive = options[option]; - break; - default: - throw new Error(`Unknown option: ${option}`); + case 'language': + fullOptions.$language = options[option]; + break; + case 'caseSensitive': + fullOptions.$caseSensitive = options[option]; + break; + case 'diacriticSensitive': + fullOptions.$diacriticSensitive = options[option]; + break; + default: + throw new Error(`Unknown option: ${option}`); } } @@ -1961,8 +1994,8 @@ class ParseQuery { subqueryReadPreference?: string ): ParseQuery { this._readPreference = readPreference; - this._includeReadPreference = includeReadPreference; - this._subqueryReadPreference = subqueryReadPreference; + this._includeReadPreference = includeReadPreference || null; + this._subqueryReadPreference = subqueryReadPreference || null; return this; } @@ -1976,7 +2009,7 @@ class ParseQuery { async subscribe(sessionToken?: string): Promise { const currentUser = await CoreManager.getUserController().currentUserAsync(); if (!sessionToken) { - sessionToken = currentUser ? currentUser.getSessionToken() : undefined; + sessionToken = currentUser ? currentUser.getSessionToken() || undefined : undefined; } const liveQueryClient = await CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); if (liveQueryClient.shouldOpen()) { @@ -2002,7 +2035,7 @@ class ParseQuery { */ static or(...queries: Array): ParseQuery { const className = _getClassNameFromQueries(queries); - const query = new ParseQuery(className); + const query = new ParseQuery(className!); query._orQuery(queries); return query; } @@ -2021,7 +2054,7 @@ class ParseQuery { */ static and(...queries: Array): ParseQuery { const className = _getClassNameFromQueries(queries); - const query = new ParseQuery(className); + const query = new ParseQuery(className!); query._andQuery(queries); return query; } @@ -2040,7 +2073,7 @@ class ParseQuery { */ static nor(...queries: Array): ParseQuery { const className = _getClassNameFromQueries(queries); - const query = new ParseQuery(className); + const query = new ParseQuery(className!); query._norQuery(queries); return query; } @@ -2080,7 +2113,7 @@ class ParseQuery { * @param {string} name The name of query source. * @returns {Parse.Query} Returns the query, so you can chain this call. */ - fromPinWithName(name?: string): ParseQuery { + fromPinWithName(name?: string | null): ParseQuery { const localDatastore = CoreManager.getLocalDatastore(); if (localDatastore.checkIfEnabled()) { this._queriesLocalDatastore = true; @@ -2099,10 +2132,11 @@ class ParseQuery { this._xhrRequest.task._aborted = true; this._xhrRequest.task.abort(); this._xhrRequest.task = null; - this._xhrRequest.onchange = () => {}; + this._xhrRequest.onchange = () => { }; return this; } - return (this._xhrRequest.onchange = () => this.cancel()); + this._xhrRequest.onchange = () => this.cancel(); + return this; } _setRequestTask(options) { @@ -2114,12 +2148,12 @@ class ParseQuery { } const DefaultController = { - find(className: string, params: QueryJSON, options: RequestOptions): Promise> { + find(className: string, params: QueryJSON, options: RequestOptions): Promise<{ results: Array }> { const RESTController = CoreManager.getRESTController(); return RESTController.request('GET', 'classes/' + className, params, options); }, - aggregate(className: string, params: any, options: RequestOptions): Promise> { + aggregate(className: string, params: any, options: RequestOptions): Promise<{ results: Array }> { const RESTController = CoreManager.getRESTController(); return RESTController.request('GET', 'aggregate/' + className, params, options); diff --git a/src/ParseRelation.js b/src/ParseRelation.ts similarity index 92% rename from src/ParseRelation.js rename to src/ParseRelation.ts index 50c8c09f1..23b9f5c19 100644 --- a/src/ParseRelation.js +++ b/src/ParseRelation.ts @@ -20,15 +20,15 @@ import ParseQuery from './ParseQuery'; * @alias Parse.Relation */ class ParseRelation { - parent: ?ParseObject; - key: ?string; - targetClassName: ?string; + parent?: ParseObject; + key?: string; + targetClassName: string | null; /** * @param {Parse.Object} parent The parent of this relation. * @param {string} key The key for this relation on the parent. */ - constructor(parent: ?ParseObject, key: ?string) { + constructor(parent?: ParseObject, key?: string) { this.parent = parent; this.key = key; this.targetClassName = null; @@ -77,7 +77,9 @@ class ParseRelation { if (objects.length === 0) { return parent; } - parent.set(this.key, change); + if (this.key) { + parent.set(this.key, change); + } this.targetClassName = change._targetClassName; return parent; } @@ -99,7 +101,9 @@ class ParseRelation { if (objects.length === 0) { return; } - this.parent.set(this.key, change); + if (this.key) { + this.parent.set(this.key, change); + } this.targetClassName = change._targetClassName; } @@ -108,7 +112,7 @@ class ParseRelation { * * @returns {object} JSON representation of Relation */ - toJSON(): { __type: 'Relation', className: ?string } { + toJSON(): { __type: 'Relation', className: string | null } { return { __type: 'Relation', className: this.targetClassName, diff --git a/src/ParseRole.js b/src/ParseRole.ts similarity index 94% rename from src/ParseRole.js rename to src/ParseRole.ts index fee282395..9dbd00315 100644 --- a/src/ParseRole.js +++ b/src/ParseRole.ts @@ -2,9 +2,9 @@ * @flow */ +import ParseObject from './ParseObject'; import ParseACL from './ParseACL'; import ParseError from './ParseError'; -import ParseObject from './ParseObject'; import type { AttributeMap } from './ObjectStateMutations'; import type ParseRelation from './ParseRelation'; @@ -42,7 +42,7 @@ class ParseRole extends ParseObject { * * @returns {string} the name of the role. */ - getName(): ?string { + getName(): string | null { const name = this.get('name'); if (name == null || typeof name === 'string') { return name; @@ -67,7 +67,7 @@ class ParseRole extends ParseObject { * callbacks. * @returns {(ParseObject|boolean)} true if the set succeeded. */ - setName(name: string, options?: mixed): ParseObject | boolean { + setName(name: string, options?: any): ParseObject | boolean { this._validateName(name); return this.set('name', name, options); } @@ -114,8 +114,8 @@ class ParseRole extends ParseObject { } } - validate(attrs: AttributeMap, options?: mixed): ParseError | boolean { - const isInvalid = super.validate(attrs, options); + validate(attrs: AttributeMap, options?: any): ParseError | boolean { + const isInvalid = (super.validate as typeof this['validate'])(attrs, options); if (isInvalid) { return isInvalid; } @@ -142,5 +142,4 @@ class ParseRole extends ParseObject { } ParseObject.registerSubclass('_Role', ParseRole); - export default ParseRole; diff --git a/src/ParseSchema.js b/src/ParseSchema.ts similarity index 93% rename from src/ParseSchema.js rename to src/ParseSchema.ts index 089cddd32..b878736c6 100644 --- a/src/ParseSchema.js +++ b/src/ParseSchema.ts @@ -21,11 +21,13 @@ const FIELD_TYPES = [ 'Object', 'Pointer', 'Relation', -]; +] as const; +type ValidFieldType = typeof FIELD_TYPES[number]; type FieldOptions = { - required: boolean, - defaultValue: mixed, + required?: boolean, + defaultValue?: any, + targetClass?: string, }; /** @@ -47,9 +49,9 @@ type FieldOptions = { */ class ParseSchema { className: string; - _fields: { [key: string]: mixed }; - _indexes: { [key: string]: mixed }; - _clp: { [key: string]: mixed }; + _fields: { [key: string]: any }; + _indexes: { [key: string]: any }; + _clp: { [key: string]: any }; /** * @param {string} className Parse Class string. @@ -212,7 +214,7 @@ class ParseSchema { * * @returns {Parse.Schema} Returns the schema, so you can chain this call. */ - addField(name: string, type: string, options: FieldOptions = {}) { + addField(name: string, type: ValidFieldType, options: FieldOptions = {}) { type = type || 'String'; if (!name) { @@ -222,12 +224,14 @@ class ParseSchema { throw new Error(`${type} is not a valid type.`); } if (type === 'Pointer') { - return this.addPointer(name, options.targetClass, options); + return this.addPointer(name, options.targetClass!, options); } if (type === 'Relation') { - return this.addRelation(name, options.targetClass, options); + return this.addRelation(name, options.targetClass); } - const fieldOptions = { type }; + const fieldOptions: Partial & { + type: ValidFieldType, + } = { type }; if (typeof options.required === 'boolean') { fieldOptions.required = options.required; @@ -404,7 +408,9 @@ class ParseSchema { if (!targetClass) { throw new Error('You need to set the targetClass of the Pointer.'); } - const fieldOptions = { type: 'Pointer', targetClass }; + const fieldOptions: Partial & { + type: ValidFieldType, + } = { type: 'Pointer', targetClass }; if (typeof options.required === 'boolean') { fieldOptions.required = options.required; @@ -466,30 +472,30 @@ class ParseSchema { } const DefaultController = { - send(className: string, method: string, params: any = {}): Promise { + send(className: string, method: string, params: any = {}): Promise { const RESTController = CoreManager.getRESTController(); return RESTController.request(method, `schemas/${className}`, params, { useMasterKey: true, }); }, - get(className: string): Promise { + get(className: string): Promise<{ results: ParseSchema[] }> { return this.send(className, 'GET'); }, - create(className: string, params: any): Promise { + create(className: string, params: any): Promise { return this.send(className, 'POST', params); }, - update(className: string, params: any): Promise { + update(className: string, params: any): Promise { return this.send(className, 'PUT', params); }, - delete(className: string): Promise { + delete(className: string): Promise { return this.send(className, 'DELETE'); }, - purge(className: string): Promise { + purge(className: string): Promise { const RESTController = CoreManager.getRESTController(); return RESTController.request('DELETE', `purge/${className}`, {}, { useMasterKey: true }); }, diff --git a/src/ParseSession.ts b/src/ParseSession.ts index 51b001742..ab64e43a3 100644 --- a/src/ParseSession.ts +++ b/src/ParseSession.ts @@ -57,7 +57,7 @@ class ParseSession extends ParseObject { options = options || {}; const controller = CoreManager.getSessionController(); - const sessionOptions = {}; + const sessionOptions: FullOptions = {}; if (options.hasOwnProperty('useMasterKey')) { sessionOptions.useMasterKey = options.useMasterKey; } @@ -65,7 +65,7 @@ class ParseSession extends ParseObject { if (!user) { return Promise.reject('There is no current user.'); } - sessionOptions.sessionToken = user.getSessionToken(); + sessionOptions.sessionToken = user.getSessionToken() || undefined; return controller.getSession(sessionOptions); }); } diff --git a/src/ParseUser.js b/src/ParseUser.ts similarity index 92% rename from src/ParseUser.js rename to src/ParseUser.ts index 8ba9ba8c0..1bd4bf1af 100644 --- a/src/ParseUser.js +++ b/src/ParseUser.ts @@ -12,14 +12,27 @@ import Storage from './Storage'; import type { AttributeMap } from './ObjectStateMutations'; import type { RequestOptions, FullOptions } from './RESTController'; -export type AuthData = ?{ [key: string]: mixed }; +export type AuthData = { [key: string]: any }; + +export type AuthProviderType = { + authenticate?(options: { + error?: (provider: AuthProviderType, error: string | any) => void, + success?: (provider: AuthProviderType, result: AuthData) => void, + }): void, + + restoreAuthentication(authData: any): boolean; + /** Returns the AuthType of this provider */ + getAuthType(): string; + deauthenticate?(): void; +}; + const CURRENT_USER_KEY = 'currentUser'; let canUseCurrentUser = !CoreManager.get('IS_NODE'); let currentUserCacheMatchesDisk = false; -let currentUserCache = null; +let currentUserCache: ParseUser | null = null; -const authProviders = {}; +const authProviders: { [key: string]: AuthProviderType } = {}; /** *

    A Parse.User object is a local representation of a user persisted to the @@ -35,7 +48,7 @@ class ParseUser extends ParseObject { /** * @param {object} attributes The initial set of data to store in the user. */ - constructor(attributes: ?AttributeMap) { + constructor(attributes?: AttributeMap) { super('_User'); if (attributes && typeof attributes === 'object') { if (!this.set(attributes || {})) { @@ -54,7 +67,7 @@ class ParseUser extends ParseObject { _upgradeToRevocableSession(options: RequestOptions): Promise { options = options || {}; - const upgradeOptions = {}; + const upgradeOptions: RequestOptions = {}; if (options.hasOwnProperty('useMasterKey')) { upgradeOptions.useMasterKey = options.useMasterKey; } @@ -79,12 +92,12 @@ class ParseUser extends ParseObject { * @returns {Promise} A promise that is fulfilled with the user is linked */ linkWith( - provider: any, - options: { authData?: AuthData }, - saveOpts?: FullOptions = {} + provider: AuthProviderType, + options: { authData?: AuthData | null }, + saveOpts: FullOptions = {} ): Promise { saveOpts.sessionToken = saveOpts.sessionToken || this.getSessionToken() || ''; - let authType; + let authType: string; if (typeof provider === 'string') { authType = provider; if (authProviders[provider]) { @@ -121,9 +134,9 @@ class ParseUser extends ParseObject { }); } else { return new Promise((resolve, reject) => { - provider.authenticate({ + provider.authenticate!({ success: (provider, result) => { - const opts = {}; + const opts: { authData?: AuthData } = {}; opts.authData = result; this.linkWith(provider, opts, saveOpts).then( () => { @@ -152,7 +165,7 @@ class ParseUser extends ParseObject { _linkWith( provider: any, options: { authData?: AuthData }, - saveOpts?: FullOptions = {} + saveOpts: FullOptions = {} ): Promise { return this.linkWith(provider, options, saveOpts); } @@ -163,7 +176,7 @@ class ParseUser extends ParseObject { * * @param provider */ - _synchronizeAuthData(provider: string) { + _synchronizeAuthData(provider: string | AuthProviderType) { if (!this.isCurrent() || !provider) { return; } @@ -336,7 +349,7 @@ class ParseUser extends ParseObject { * * @returns {string} */ - getUsername(): ?string { + getUsername(): string | null { const username = this.get('username'); if (username == null || typeof username === 'string') { return username; @@ -368,7 +381,7 @@ class ParseUser extends ParseObject { * * @returns {string} User's Email */ - getEmail(): ?string { + getEmail(): string | null { const email = this.get('email'); if (email == null || typeof email === 'string') { return email; @@ -393,7 +406,7 @@ class ParseUser extends ParseObject { * * @returns {string} the session token, or undefined */ - getSessionToken(): ?string { + getSessionToken(): string | null { const token = this.get('sessionToken'); if (token == null || typeof token === 'string') { return token; @@ -424,10 +437,10 @@ class ParseUser extends ParseObject { * @returns {Promise} A promise that is fulfilled when the signup * finishes. */ - signUp(attrs: AttributeMap, options?: FullOptions): Promise { + signUp(attrs: AttributeMap, options?: FullOptions & { context?: AttributeMap }): Promise { options = options || {}; - const signupOptions = {}; + const signupOptions: FullOptions & { context?: AttributeMap } = {}; if (options.hasOwnProperty('useMasterKey')) { signupOptions.useMasterKey = options.useMasterKey; } @@ -456,10 +469,9 @@ class ParseUser extends ParseObject { * @returns {Promise} A promise that is fulfilled with the user when * the login is complete. */ - logIn(options?: FullOptions): Promise { - options = options || {}; + logIn(options: FullOptions & { context?: AttributeMap } = {}): Promise { - const loginOptions = { usePost: true }; + const loginOptions: { usePost: boolean, context?: AttributeMap } & FullOptions = { usePost: true }; if (options.hasOwnProperty('useMasterKey')) { loginOptions.useMasterKey = options.useMasterKey; } @@ -467,7 +479,7 @@ class ParseUser extends ParseObject { loginOptions.installationId = options.installationId; } if (options.hasOwnProperty('usePost')) { - loginOptions.usePost = options.usePost; + loginOptions.usePost = options.usePost!; } if ( options.hasOwnProperty('context') && @@ -486,11 +498,11 @@ class ParseUser extends ParseObject { * @param {...any} args * @returns {Promise} */ - async save(...args: Array): Promise { + async save(...args: Array): Promise { await super.save.apply(this, args); const current = await this.isCurrentAsync(); if (current) { - return CoreManager.getUserController().updateUserOnDisk(this); + return CoreManager.getUserController().updateUserOnDisk(this) as Promise; } return this; } @@ -500,9 +512,9 @@ class ParseUser extends ParseObject { * the current user when it is destroyed * * @param {...any} args - * @returns {Parse.User} + * @returns {Parse.User|void} */ - async destroy(...args: Array): Promise { + async destroy(...args: Array): Promise { await super.destroy.apply(this, args); const current = await this.isCurrentAsync(); if (current) { @@ -606,7 +618,7 @@ class ParseUser extends ParseObject { * @static * @returns {Parse.Object} The currently logged in Parse.User. */ - static current(): ?ParseUser { + static current(): ParseUser | null { if (!canUseCurrentUser) { return null; } @@ -621,7 +633,7 @@ class ParseUser extends ParseObject { * @returns {Promise} A Promise that is resolved with the currently * logged in Parse User */ - static currentAsync(): Promise { + static currentAsync(): Promise { if (!canUseCurrentUser) { return Promise.resolve(null); } @@ -758,7 +770,7 @@ class ParseUser extends ParseObject { * @static * @returns {Promise} A promise that is fulfilled with the user is fetched. */ - static me(sessionToken: string, options?: RequestOptions = {}) { + static me(sessionToken: string, options: RequestOptions = {}) { const controller = CoreManager.getUserController(); const meOptions: RequestOptions = { sessionToken: sessionToken, @@ -833,7 +845,7 @@ class ParseUser extends ParseObject { static requestPasswordReset(email: string, options?: RequestOptions) { options = options || {}; - const requestOptions = {}; + const requestOptions: { useMasterKey?: boolean } = {}; if (options.hasOwnProperty('useMasterKey')) { requestOptions.useMasterKey = options.useMasterKey; } @@ -854,7 +866,7 @@ class ParseUser extends ParseObject { static requestEmailVerification(email: string, options?: RequestOptions) { options = options || {}; - const requestOptions = {}; + const requestOptions: RequestOptions = {}; if (options.hasOwnProperty('useMasterKey')) { requestOptions.useMasterKey = options.useMasterKey; } @@ -873,7 +885,7 @@ class ParseUser extends ParseObject { * @returns {Promise} A promise that is fulfilled with a user * when the password is correct. */ - static verifyPassword(username: string, password: string, options?: RequestOptions) { + static verifyPassword(username: string, password: string, options?: RequestOptions): Promise { if (typeof username !== 'string') { return Promise.reject(new ParseError(ParseError.OTHER_CAUSE, 'Username must be a string.')); } @@ -884,7 +896,7 @@ class ParseUser extends ParseObject { options = options || {}; - const verificationOption = {}; + const verificationOption: RequestOptions = {}; if (options.hasOwnProperty('useMasterKey')) { verificationOption.useMasterKey = options.useMasterKey; } @@ -1032,7 +1044,7 @@ const DefaultController = { return DefaultController.updateUserOnDisk(user); }, - currentUser(): ?ParseUser { + currentUser(): ParseUser | null { if (currentUserCache) { return currentUserCache; } @@ -1042,7 +1054,7 @@ const DefaultController = { if (Storage.async()) { throw new Error( 'Cannot call currentUser() when using a platform with an async ' + - 'storage system. Call currentUserAsync() instead.' + 'storage system. Call currentUserAsync() instead.' ); } const path = Storage.generatePath(CURRENT_USER_KEY); @@ -1056,27 +1068,27 @@ const DefaultController = { const crypto = CoreManager.getCryptoController(); userData = crypto.decrypt(userData, CoreManager.get('ENCRYPTED_KEY')); } - userData = JSON.parse(userData); - if (!userData.className) { - userData.className = '_User'; + const userDataObj = JSON.parse(userData); + if (!userDataObj.className) { + userDataObj.className = '_User'; } - if (userData._id) { - if (userData.objectId !== userData._id) { - userData.objectId = userData._id; + if (userDataObj._id) { + if (userDataObj.objectId !== userDataObj._id) { + userDataObj.objectId = userDataObj._id; } - delete userData._id; + delete userDataObj._id; } - if (userData._sessionToken) { - userData.sessionToken = userData._sessionToken; - delete userData._sessionToken; + if (userDataObj._sessionToken) { + userDataObj.sessionToken = userDataObj._sessionToken; + delete userDataObj._sessionToken; } - const current = ParseObject.fromJSON(userData); + const current = ParseObject.fromJSON(userDataObj) as ParseUser; currentUserCache = current; current._synchronizeAllAuthData(); return current; }, - currentUserAsync(): Promise { + currentUserAsync(): Promise { if (currentUserCache) { return Promise.resolve(currentUserCache); } @@ -1094,21 +1106,21 @@ const DefaultController = { const crypto = CoreManager.getCryptoController(); userData = crypto.decrypt(userData.toString(), CoreManager.get('ENCRYPTED_KEY')); } - userData = JSON.parse(userData); - if (!userData.className) { - userData.className = '_User'; + const userDataObj = JSON.parse(userData); + if (!userDataObj.className) { + userDataObj.className = '_User'; } - if (userData._id) { - if (userData.objectId !== userData._id) { - userData.objectId = userData._id; + if (userDataObj._id) { + if (userDataObj.objectId !== userDataObj._id) { + userDataObj.objectId = userDataObj._id; } - delete userData._id; + delete userDataObj._id; } - if (userData._sessionToken) { - userData.sessionToken = userData._sessionToken; - delete userData._sessionToken; + if (userDataObj._sessionToken) { + userDataObj.sessionToken = userDataObj._sessionToken; + delete userDataObj._sessionToken; } - const current = ParseObject.fromJSON(userData); + const current = ParseObject.fromJSON(userDataObj) as ParseUser; currentUserCache = current; current._synchronizeAllAuthData(); return Promise.resolve(current); @@ -1206,7 +1218,7 @@ const DefaultController = { }); }, - logOut(options: RequestOptions): Promise { + logOut(options: RequestOptions): Promise { const RESTController = CoreManager.getRESTController(); if (options.sessionToken) { return RESTController.request('POST', 'logout', {}, options); @@ -1258,7 +1270,7 @@ const DefaultController = { return Promise.resolve(user); }, - linkWith(user: ParseUser, authData: AuthData, options: FullOptions) { + linkWith(user: ParseUser, authData: AuthData, options?: FullOptions) { return user.save({ authData }, options).then(() => { if (canUseCurrentUser) { return DefaultController.setCurrentUser(user); diff --git a/src/Push.js b/src/Push.ts similarity index 86% rename from src/Push.js rename to src/Push.ts index a565396be..d6b91b977 100644 --- a/src/Push.js +++ b/src/Push.ts @@ -3,6 +3,7 @@ */ import CoreManager from './CoreManager'; +import ParseObject from './ParseObject'; import ParseQuery from './ParseQuery'; import type { WhereClause } from './ParseQuery'; @@ -49,9 +50,9 @@ export type PushData = { * be used for this request. * * @returns {Promise} A promise that is fulfilled when the push request - * completes. + * completes., returns `pushStatusId` */ -export function send(data: PushData, options?: FullOptions = {}): Promise { +export function send(data: PushData, options: FullOptions = {}): Promise { if (data.where && data.where instanceof ParseQuery) { data.where = data.where.toJSON().where; } @@ -70,7 +71,7 @@ export function send(data: PushData, options?: FullOptions = {}): Promise { const pushOptions = { useMasterKey: true }; if (options.hasOwnProperty('useMasterKey')) { - pushOptions.useMasterKey = options.useMasterKey; + pushOptions.useMasterKey = options.useMasterKey!; } return CoreManager.getPushController().send(data, pushOptions); @@ -89,10 +90,10 @@ export function send(data: PushData, options?: FullOptions = {}): Promise { * * @returns {Parse.Object} Status of Push. */ -export function getPushStatus(pushStatusId: string, options?: FullOptions = {}): Promise { +export function getPushStatus(pushStatusId: string, options: FullOptions = {}): Promise { const pushOptions = { useMasterKey: true }; if (options.hasOwnProperty('useMasterKey')) { - pushOptions.useMasterKey = options.useMasterKey; + pushOptions.useMasterKey = options.useMasterKey!; } const query = new ParseQuery('_PushStatus'); return query.get(pushStatusId, pushOptions); @@ -100,8 +101,8 @@ export function getPushStatus(pushStatusId: string, options?: FullOptions = {}): const DefaultController = { async send(data: PushData, options?: FullOptions) { - options.returnStatus = true; - const response = await CoreManager.getRESTController().request('POST', 'push', data, options); + const myOptions = { ...options, returnStatus: true }; + const response = await CoreManager.getRESTController().request('POST', 'push', data, myOptions); return response._headers?.['X-Parse-Push-Status-Id']; }, }; diff --git a/src/RESTController.js b/src/RESTController.ts similarity index 91% rename from src/RESTController.js rename to src/RESTController.ts index 75b30a759..c0ce2649f 100644 --- a/src/RESTController.js +++ b/src/RESTController.ts @@ -2,7 +2,7 @@ * @flow */ /* global XMLHttpRequest, XDomainRequest */ -const uuidv4 = require('./uuid'); +import uuidv4 from './uuid'; import CoreManager from './CoreManager'; import ParseError from './ParseError'; @@ -28,9 +28,10 @@ export type FullOptions = { installationId?: string, progress?: any, usePost?: boolean, + requestTask?: any, }; -let XHR = null; +let XHR: typeof XMLHttpRequest = null as any; if (typeof XMLHttpRequest !== 'undefined') { XHR = XMLHttpRequest; } @@ -42,12 +43,14 @@ if (process.env.PARSE_BUILD === 'weapp') { } let useXDomainRequest = false; +// @ts-ignore if (typeof XDomainRequest !== 'undefined' && !('withCredentials' in new XMLHttpRequest())) { useXDomainRequest = true; } function ajaxIE9(method: string, url: string, data: any, headers?: any, options?: FullOptions) { return new Promise((resolve, reject) => { + // @ts-ignore const xdr = new XDomainRequest(); xdr.onload = function () { let response; @@ -77,7 +80,9 @@ function ajaxIE9(method: string, url: string, data: any, headers?: any, options? }; xdr.open(method, url); xdr.send(data); + // @ts-ignore if (options && typeof options.requestTask === 'function') { + // @ts-ignore options.requestTask(xdr); } }); @@ -101,7 +106,7 @@ const RESTController = { const xhr = new XHR(); xhr.onreadystatechange = function () { - if (xhr.readyState !== 4 || handled || xhr._aborted) { + if (xhr.readyState !== 4 || handled || (xhr as any)._aborted) { return; } handled = true; @@ -112,8 +117,8 @@ const RESTController = { response = JSON.parse(xhr.responseText); headers = {}; if (typeof xhr.getResponseHeader === 'function' && xhr.getResponseHeader('access-control-expose-headers')) { - const responseHeaders = xhr.getResponseHeader('access-control-expose-headers').split(', '); - responseHeaders.forEach(header => { + const responseHeaders = xhr.getResponseHeader('access-control-expose-headers')?.split(', '); + responseHeaders?.forEach(header => { headers[header] = xhr.getResponseHeader(header.toLowerCase()); }); } @@ -203,7 +208,7 @@ const RESTController = { return promise; }, - request(method: string, path: string, data: mixed, options?: RequestOptions) { + request(method: string, path: string, data: any, options?: RequestOptions) { options = options || {}; let url = CoreManager.get('SERVER_URL'); if (url[url.length - 1] !== '/') { @@ -211,7 +216,18 @@ const RESTController = { } url += path; - const payload = {}; + type PayloadType = { + _context?: any, + _method?: string, + _ApplicationId: string, + _JavaScriptKey?: string, + _ClientVersion: string, + _MasterKey?: string, + _RevocableSession?: string, + _InstallationId?: string, + _SessionToken?: string, + }; + const payload: Partial = {}; if (data && typeof data === 'object') { for (const k in data) { payload[k] = data[k]; @@ -254,7 +270,7 @@ const RESTController = { } const installationId = options.installationId; - let installationIdPromise; + let installationIdPromise: Promise; if (installationId && typeof installationId === 'string') { installationIdPromise = Promise.resolve(installationId); } else { @@ -297,7 +313,7 @@ const RESTController = { .catch(RESTController.handleError); }, - handleError(response) { + handleError(response: any) { // Transform the error into an instance of ParseError by trying to parse // the error string as JSON let error; @@ -332,3 +348,4 @@ const RESTController = { }; module.exports = RESTController; +export default RESTController; diff --git a/src/SingleInstanceStateController.js b/src/SingleInstanceStateController.ts similarity index 92% rename from src/SingleInstanceStateController.js rename to src/SingleInstanceStateController.ts index 3dd9c2cc7..3be62c78d 100644 --- a/src/SingleInstanceStateController.js +++ b/src/SingleInstanceStateController.ts @@ -18,7 +18,7 @@ let objectState: { }, } = {}; -export function getState(obj: ObjectIdentifier): ?State { +export function getState(obj: ObjectIdentifier): State | null { const classData = objectState[obj.className]; if (classData) { return classData[obj.id] || null; @@ -41,7 +41,7 @@ export function initializeState(obj: ObjectIdentifier, initial?: State): State { return state; } -export function removeState(obj: ObjectIdentifier): ?State { +export function removeState(obj: ObjectIdentifier): State | null { const state = getState(obj); if (state === null) { return null; @@ -71,7 +71,7 @@ export function getPendingOps(obj: ObjectIdentifier): Array { return [{}]; } -export function setPendingOp(obj: ObjectIdentifier, attr: string, op: ?Op) { +export function setPendingOp(obj: ObjectIdentifier, attr: string, op?: Op) { const pendingOps = initializeState(obj).pendingOps; ObjectStateMutations.setPendingOp(pendingOps, attr, op); } @@ -81,7 +81,7 @@ export function pushPendingState(obj: ObjectIdentifier) { ObjectStateMutations.pushPendingState(pendingOps); } -export function popPendingState(obj: ObjectIdentifier): OpsMap { +export function popPendingState(obj: ObjectIdentifier): OpsMap | undefined { const pendingOps = initializeState(obj).pendingOps; return ObjectStateMutations.popPendingState(pendingOps); } @@ -99,7 +99,7 @@ export function getObjectCache(obj: ObjectIdentifier): ObjectCache { return {}; } -export function estimateAttribute(obj: ObjectIdentifier, attr: string): mixed { +export function estimateAttribute(obj: ObjectIdentifier, attr: string): any { const serverData = getServerData(obj); const pendingOps = getPendingOps(obj); return ObjectStateMutations.estimateAttribute( @@ -122,7 +122,7 @@ export function commitServerChanges(obj: ObjectIdentifier, changes: AttributeMap ObjectStateMutations.commitServerChanges(state.serverData, state.objectCache, changes); } -export function enqueueTask(obj: ObjectIdentifier, task: () => Promise): Promise { +export function enqueueTask(obj: ObjectIdentifier, task: () => Promise): Promise { const state = initializeState(obj); return state.tasks.enqueue(task); } diff --git a/src/Socket.weapp.js b/src/Socket.weapp.ts similarity index 64% rename from src/Socket.weapp.js rename to src/Socket.weapp.ts index d8d3884e7..239180300 100644 --- a/src/Socket.weapp.js +++ b/src/Socket.weapp.ts @@ -1,36 +1,51 @@ module.exports = class SocketWeapp { - constructor(serverURL) { + onopen: () => void; + onmessage: () => void; + onclose: () => void; + onerror: () => void; + + constructor(serverURL: string) { this.onopen = () => {}; this.onmessage = () => {}; this.onclose = () => {}; this.onerror = () => {}; + // @ts-ignore wx.onSocketOpen(() => { this.onopen(); }); + // @ts-ignore wx.onSocketMessage(msg => { + // @ts-ignore this.onmessage(msg); }); + // @ts-ignore wx.onSocketClose((event) => { + // @ts-ignore this.onclose(event); }); + // @ts-ignore wx.onSocketError(error => { + // @ts-ignore this.onerror(error); }); + // @ts-ignore wx.connectSocket({ url: serverURL, }); } send(data) { + // @ts-ignore wx.sendSocketMessage({ data }); } close() { + // @ts-ignore wx.closeSocket(); } }; diff --git a/src/Storage.js b/src/Storage.ts similarity index 73% rename from src/Storage.js rename to src/Storage.ts index 67392c249..bcbd094c9 100644 --- a/src/Storage.js +++ b/src/Storage.ts @@ -4,13 +4,39 @@ import CoreManager from './CoreManager'; +export type StorageController = + | { + async: 0, + getItem: (path: string) => string | null, + setItem: (path: string, value: string) => void, + removeItem: (path: string) => void, + getItemAsync?: (path: string) => Promise, + setItemAsync?: (path: string, value: string) => Promise, + removeItemAsync?: (path: string) => Promise, + clear: () => void, + getAllKeys?: () => Array + getAllKeysAsync?: () => Promise> + } + | { + async: 1, + getItem?: (path: string) => string | null, + setItem?: (path: string, value: string) => void, + removeItem?: (path: string) => void, + getItemAsync: (path: string) => Promise, + setItemAsync: (path: string, value: string) => Promise, + removeItemAsync: (path: string) => Promise, + clear: () => void, + getAllKeys?: () => Array + getAllKeysAsync?: () => Promise> + }; + const Storage = { async(): boolean { const controller = CoreManager.getStorageController(); return !!controller.async; }, - getItem(path: string): ?string { + getItem(path: string): string | null { const controller = CoreManager.getStorageController(); if (controller.async === 1) { throw new Error('Synchronous storage is not supported by the current storage controller'); @@ -18,7 +44,7 @@ const Storage = { return controller.getItem(path); }, - getItemAsync(path: string): Promise { + getItemAsync(path: string): Promise { const controller = CoreManager.getStorageController(); if (controller.async === 1) { return controller.getItemAsync(path); @@ -63,15 +89,15 @@ const Storage = { if (controller.async === 1) { throw new Error('Synchronous storage is not supported by the current storage controller'); } - return controller.getAllKeys(); + return controller.getAllKeys!(); }, getAllKeysAsync(): Promise> { const controller = CoreManager.getStorageController(); if (controller.async === 1) { - return controller.getAllKeysAsync(); + return controller.getAllKeysAsync!(); } - return Promise.resolve(controller.getAllKeys()); + return Promise.resolve(controller.getAllKeys!()); }, generatePath(path: string): string { diff --git a/src/StorageController.browser.js b/src/StorageController.browser.ts similarity index 80% rename from src/StorageController.browser.js rename to src/StorageController.browser.ts index 0fdcd37b7..275d63715 100644 --- a/src/StorageController.browser.js +++ b/src/StorageController.browser.ts @@ -7,7 +7,7 @@ const StorageController = { async: 0, - getItem(path: string): ?string { + getItem(path: string): string | null { return localStorage.getItem(path); }, @@ -25,9 +25,9 @@ const StorageController = { }, getAllKeys() { - const keys = []; + const keys: string[] = []; for (let i = 0; i < localStorage.length; i += 1) { - keys.push(localStorage.key(i)); + keys.push(localStorage.key(i) as string); } return keys; }, @@ -38,3 +38,4 @@ const StorageController = { }; module.exports = StorageController; +export default StorageController; diff --git a/src/StorageController.default.js b/src/StorageController.default.ts similarity index 89% rename from src/StorageController.default.js rename to src/StorageController.default.ts index cb21dd54f..c6ab92ceb 100644 --- a/src/StorageController.default.js +++ b/src/StorageController.default.ts @@ -8,7 +8,7 @@ const memMap = {}; const StorageController = { async: 0, - getItem(path: string): ?string { + getItem(path: string): string | null { if (memMap.hasOwnProperty(path)) { return memMap[path]; } @@ -37,3 +37,4 @@ const StorageController = { }; module.exports = StorageController; +export default StorageController; diff --git a/src/StorageController.react-native.js b/src/StorageController.react-native.ts similarity index 51% rename from src/StorageController.react-native.js rename to src/StorageController.react-native.ts index e3ee63dce..f44b72bbb 100644 --- a/src/StorageController.react-native.js +++ b/src/StorageController.react-native.ts @@ -7,33 +7,33 @@ import CoreManager from './CoreManager'; const StorageController = { async: 1, - getItemAsync(path: string): Promise { + getItemAsync(path: string): Promise { return new Promise((resolve, reject) => { - CoreManager.getAsyncStorage().getItem(path, (err, value) => { + CoreManager.getAsyncStorage()!.getItem(path, (err, value) => { if (err) { reject(err); } else { - resolve(value); + resolve(value || null); } }); }); }, - setItemAsync(path: string, value: string): Promise { + setItemAsync(path: string, value: string): Promise { return new Promise((resolve, reject) => { - CoreManager.getAsyncStorage().setItem(path, value, (err, value) => { + CoreManager.getAsyncStorage()!.setItem(path, value, (err) => { if (err) { reject(err); } else { - resolve(value); + resolve(); } }); }); }, - removeItemAsync(path: string): Promise { + removeItemAsync(path: string): Promise { return new Promise((resolve, reject) => { - CoreManager.getAsyncStorage().removeItem(path, err => { + CoreManager.getAsyncStorage()!.removeItem(path, err => { if (err) { reject(err); } else { @@ -43,33 +43,33 @@ const StorageController = { }); }, - getAllKeysAsync(): Promise { + getAllKeysAsync(): Promise { return new Promise((resolve, reject) => { - CoreManager.getAsyncStorage().getAllKeys((err, keys) => { + CoreManager.getAsyncStorage()!.getAllKeys((err, keys) => { if (err) { reject(err); } else { - resolve(keys); + resolve(keys || null); } }); }); }, - multiGet(keys: Array): Promise>> { + multiGet(keys: Array): Promise { return new Promise((resolve, reject) => { - CoreManager.getAsyncStorage().multiGet(keys, (err, result) => { + CoreManager.getAsyncStorage()!.multiGet(keys, (err, result) => { if (err) { reject(err); } else { - resolve(result); + resolve(result || null); } }); }); }, - multiRemove(keys: Array): Promise { + multiRemove(keys: Array): Promise> { return new Promise((resolve, reject) => { - CoreManager.getAsyncStorage().multiRemove(keys, err => { + CoreManager.getAsyncStorage()!.multiRemove(keys, err => { if (err) { reject(err); } else { @@ -80,8 +80,9 @@ const StorageController = { }, clear() { - return CoreManager.getAsyncStorage().clear(); + return CoreManager.getAsyncStorage()!.clear(); }, }; module.exports = StorageController; +export default StorageController; diff --git a/src/StorageController.weapp.js b/src/StorageController.weapp.ts similarity index 78% rename from src/StorageController.weapp.js rename to src/StorageController.weapp.ts index 321172c4e..c987c3b40 100644 --- a/src/StorageController.weapp.js +++ b/src/StorageController.weapp.ts @@ -6,12 +6,14 @@ const StorageController = { async: 0, - getItem(path: string): ?string { + getItem(path: string): string | null { + // @ts-ignore return wx.getStorageSync(path); }, setItem(path: string, value: string) { try { + // @ts-ignore wx.setStorageSync(path, value); } catch (e) { // Quota exceeded @@ -19,15 +21,18 @@ const StorageController = { }, removeItem(path: string) { + // @ts-ignore wx.removeStorageSync(path); }, getAllKeys() { + // @ts-ignore const res = wx.getStorageInfoSync(); return res.keys; }, clear() { + // @ts-ignore wx.clearStorageSync(); }, }; diff --git a/src/TaskQueue.js b/src/TaskQueue.ts similarity index 80% rename from src/TaskQueue.js rename to src/TaskQueue.ts index eedd769fe..55d45b757 100644 --- a/src/TaskQueue.js +++ b/src/TaskQueue.ts @@ -4,8 +4,8 @@ import { resolvingPromise } from './promiseUtils'; type Task = { - task: () => Promise, - _completion: Promise, + task: () => Promise, + _completion: ReturnType>, }; class TaskQueue { @@ -15,8 +15,8 @@ class TaskQueue { this.queue = []; } - enqueue(task: () => Promise): Promise { - const taskComplete = new resolvingPromise(); + enqueue(task: () => Promise): Promise { + const taskComplete = resolvingPromise(); this.queue.push({ task: task, _completion: taskComplete, @@ -55,3 +55,4 @@ class TaskQueue { } module.exports = TaskQueue; +export default TaskQueue; diff --git a/src/UniqueInstanceStateController.js b/src/UniqueInstanceStateController.ts similarity index 92% rename from src/UniqueInstanceStateController.js rename to src/UniqueInstanceStateController.ts index aaf21da10..56763e3ae 100644 --- a/src/UniqueInstanceStateController.js +++ b/src/UniqueInstanceStateController.ts @@ -11,7 +11,7 @@ import type { AttributeMap, ObjectCache, OpsMap, State } from './ObjectStateMuta let objectState = new WeakMap(); -export function getState(obj: ParseObject): ?State { +export function getState(obj: ParseObject): State | null { const classData = objectState.get(obj); return classData || null; } @@ -35,7 +35,7 @@ export function initializeState(obj: ParseObject, initial?: State): State { return state; } -export function removeState(obj: ParseObject): ?State { +export function removeState(obj: ParseObject): State | null { const state = getState(obj); if (state === null) { return null; @@ -65,7 +65,7 @@ export function getPendingOps(obj: ParseObject): Array { return [{}]; } -export function setPendingOp(obj: ParseObject, attr: string, op: ?Op) { +export function setPendingOp(obj: ParseObject, attr: string, op?: Op) { const pendingOps = initializeState(obj).pendingOps; ObjectStateMutations.setPendingOp(pendingOps, attr, op); } @@ -75,7 +75,7 @@ export function pushPendingState(obj: ParseObject) { ObjectStateMutations.pushPendingState(pendingOps); } -export function popPendingState(obj: ParseObject): OpsMap { +export function popPendingState(obj: ParseObject): OpsMap | undefined { const pendingOps = initializeState(obj).pendingOps; return ObjectStateMutations.popPendingState(pendingOps); } @@ -93,7 +93,7 @@ export function getObjectCache(obj: ParseObject): ObjectCache { return {}; } -export function estimateAttribute(obj: ParseObject, attr: string): mixed { +export function estimateAttribute(obj: ParseObject, attr: string): any { const serverData = getServerData(obj); const pendingOps = getPendingOps(obj); return ObjectStateMutations.estimateAttribute( @@ -116,7 +116,7 @@ export function commitServerChanges(obj: ParseObject, changes: AttributeMap) { ObjectStateMutations.commitServerChanges(state.serverData, state.objectCache, changes); } -export function enqueueTask(obj: ParseObject, task: () => Promise): Promise { +export function enqueueTask(obj: ParseObject, task: () => Promise): Promise { const state = initializeState(obj); return state.tasks.enqueue(task); } diff --git a/src/Xhr.weapp.js b/src/Xhr.weapp.ts similarity index 81% rename from src/Xhr.weapp.js rename to src/Xhr.weapp.ts index cb9f90121..033fe5267 100644 --- a/src/Xhr.weapp.js +++ b/src/Xhr.weapp.ts @@ -1,4 +1,24 @@ module.exports = class XhrWeapp { + UNSENT: number; + OPENED: number; + HEADERS_RECEIVED: number; + LOADING: number; + DONE: number; + header: {}; + readyState: any; + status: number; + response: string; + responseType: string; + responseText: string; + responseHeader: {}; + method: string; + url: string; + onabort: () => void; + onprogress: () => void; + onerror: () => void; + onreadystatechange: () => void; + requestTask: any; + constructor() { this.UNSENT = 0; this.OPENED = 1; @@ -55,6 +75,7 @@ module.exports = class XhrWeapp { } send(data) { + // @ts-ignore this.requestTask = wx.request({ url: this.url, method: this.method, @@ -71,6 +92,7 @@ module.exports = class XhrWeapp { }, fail: err => { this.requestTask = null; + // @ts-ignore this.onerror(err); }, }); @@ -80,6 +102,7 @@ module.exports = class XhrWeapp { loaded: res.totalBytesWritten, total: res.totalBytesExpectedToWrite, }; + // @ts-ignore this.onprogress(event); }); } diff --git a/src/__tests__/escape-test.js b/src/__tests__/escape-test.js index 11e6e07af..767d5723a 100644 --- a/src/__tests__/escape-test.js +++ b/src/__tests__/escape-test.js @@ -1,6 +1,6 @@ jest.autoMockOff(); -const escape = require('../escape.js').default; +const escape = require('../escape').default; describe('escape', () => { it('escapes special HTML characters', () => { diff --git a/src/arrayContainsObject.js b/src/arrayContainsObject.ts similarity index 96% rename from src/arrayContainsObject.js rename to src/arrayContainsObject.ts index ca9356849..9682cf108 100644 --- a/src/arrayContainsObject.js +++ b/src/arrayContainsObject.ts @@ -1,7 +1,3 @@ -/** - * @flow - */ - import ParseObject from './ParseObject'; export default function arrayContainsObject(array: Array, object: ParseObject): boolean { diff --git a/src/canBeSerialized.js b/src/canBeSerialized.ts similarity index 100% rename from src/canBeSerialized.js rename to src/canBeSerialized.ts diff --git a/src/decode.js b/src/decode.ts similarity index 90% rename from src/decode.js rename to src/decode.ts index 557002015..4fc5efd6b 100644 --- a/src/decode.js +++ b/src/decode.ts @@ -1,7 +1,6 @@ /** * @flow */ -import ParseACL from './ParseACL'; // eslint-disable-line no-unused-vars import ParseFile from './ParseFile'; import ParseGeoPoint from './ParseGeoPoint'; import ParsePolygon from './ParsePolygon'; @@ -9,12 +8,13 @@ import ParseObject from './ParseObject'; import { opFromJSON } from './ParseOp'; import ParseRelation from './ParseRelation'; +/** Decodes values from storage type */ export default function decode(value: any): any { if (value === null || typeof value !== 'object' || value instanceof Date) { return value; } if (Array.isArray(value)) { - const dup = []; + const dup: any[] = []; value.forEach((v, i) => { dup[i] = decode(v); }); @@ -31,7 +31,7 @@ export default function decode(value: any): any { } if (value.__type === 'Relation') { // The parent and key fields will be populated by the parent - const relation = new ParseRelation(null, null); + const relation = new ParseRelation(null, null); // null, null; since tests expect this. relation.targetClassName = value.className; return relation; } diff --git a/src/encode.js b/src/encode.ts similarity index 93% rename from src/encode.js rename to src/encode.ts index 0214990f5..140d54aa5 100644 --- a/src/encode.js +++ b/src/encode.ts @@ -11,11 +11,11 @@ import { Op } from './ParseOp'; import ParseRelation from './ParseRelation'; function encode( - value: mixed, + value: any, disallowObjects: boolean, forcePointers: boolean, - seen: Array, - offline: boolean + seen: Array, + offline?: boolean ): any { if (value instanceof ParseObject) { if (disallowObjects) { @@ -56,7 +56,7 @@ function encode( if (isNaN(value)) { throw new Error('Tried to encode an invalid date.'); } - return { __type: 'Date', iso: (value: any).toJSON() }; + return { __type: 'Date', iso: (value as Date).toJSON() }; } if ( Object.prototype.toString.call(value) === '[object RegExp]' && @@ -83,10 +83,10 @@ function encode( } export default function ( - value: mixed, + value: any, disallowObjects?: boolean, forcePointers?: boolean, - seen?: Array, + seen?: Array, offline?: boolean ): any { return encode(value, !!disallowObjects, !!forcePointers, seen || [], offline); diff --git a/src/equals.js b/src/equals.ts similarity index 95% rename from src/equals.js rename to src/equals.ts index 3af927c99..234141519 100644 --- a/src/equals.js +++ b/src/equals.ts @@ -3,7 +3,7 @@ import ParseFile from './ParseFile'; import ParseGeoPoint from './ParseGeoPoint'; import ParseObject from './ParseObject'; -export default function equals(a, b) { +export default function equals(a: any, b: any): boolean { const toString = Object.prototype.toString; if (toString.call(a) === '[object Date]' || toString.call(b) === '[object Date]') { const dateA = new Date(a); diff --git a/src/escape.js b/src/escape.ts similarity index 100% rename from src/escape.js rename to src/escape.ts diff --git a/src/isRevocableSession.js b/src/isRevocableSession.ts similarity index 100% rename from src/isRevocableSession.js rename to src/isRevocableSession.ts diff --git a/src/parseDate.js b/src/parseDate.ts similarity index 90% rename from src/parseDate.js rename to src/parseDate.ts index 04ff6ea7f..8de789d67 100644 --- a/src/parseDate.js +++ b/src/parseDate.ts @@ -2,7 +2,7 @@ * @flow */ -export default function parseDate(iso8601: string): ?Date { +export default function parseDate(iso8601: string): Date | null { const regexp = new RegExp( '^([0-9]{1,4})-([0-9]{1,2})-([0-9]{1,2})' + 'T' + diff --git a/src/promiseUtils.js b/src/promiseUtils.ts similarity index 62% rename from src/promiseUtils.js rename to src/promiseUtils.ts index 176c32a0b..1302776b2 100644 --- a/src/promiseUtils.js +++ b/src/promiseUtils.ts @@ -1,30 +1,31 @@ // Create Deferred Promise -export function resolvingPromise() { - let res; - let rej; - const promise = new Promise((resolve, reject) => { +export function resolvingPromise() { + let res: (val: T) => void; + let rej: (err: any) => void; + const promise = new Promise((resolve, reject) => { res = resolve; rej = reject; }); - promise.resolve = res; - promise.reject = rej; - return promise; + const ret: typeof promise & { resolve: (res: T) => void, reject: (err: any) => void } = promise as any; + ret.resolve = res!; + ret.reject = rej!; + return ret; } -export function when(promises) { - let objects; +export function when(promises: Promise | (Promise[]), ...others: Promise[]) { + let objects: Promise[]; const arrayArgument = Array.isArray(promises); if (arrayArgument) { objects = promises; } else { - objects = arguments; + objects = arguments as any as Promise[]; } let total = objects.length; let hadError = false; - const results = []; + const results: any[] = []; const returnValue = arrayArgument ? [results] : results; - const errors = []; + const errors: any[] = []; results.length = objects.length; errors.length = objects.length; @@ -32,7 +33,7 @@ export function when(promises) { return Promise.resolve(returnValue); } - const promise = new resolvingPromise(); + const promise = resolvingPromise(); const resolveOne = function () { total--; @@ -45,7 +46,7 @@ export function when(promises) { } }; - const chain = function (object, index) { + const chain = function (object: Promise, index: number) { if (object && typeof object.then === 'function') { object.then( function (result) { @@ -66,11 +67,10 @@ export function when(promises) { for (let i = 0; i < objects.length; i++) { chain(objects[i], i); } - return promise; } -export function continueWhile(test, emitter) { +export function continueWhile(test: () => boolean, emitter: () => Promise) { if (test()) { return emitter().then(() => { return continueWhile(test, emitter); diff --git a/src/unique.js b/src/unique.ts similarity index 94% rename from src/unique.js rename to src/unique.ts index 169c288f5..1ee8f2cb1 100644 --- a/src/unique.js +++ b/src/unique.ts @@ -6,7 +6,7 @@ import arrayContainsObject from './arrayContainsObject'; import ParseObject from './ParseObject'; export default function unique(arr: Array): Array { - const uniques = []; + const uniques: T[] = []; arr.forEach(value => { if (value instanceof ParseObject) { if (!arrayContainsObject(uniques, value)) { diff --git a/src/unsavedChildren.js b/src/unsavedChildren.ts similarity index 90% rename from src/unsavedChildren.js rename to src/unsavedChildren.ts index 088bd6084..9a55c722e 100644 --- a/src/unsavedChildren.js +++ b/src/unsavedChildren.ts @@ -24,7 +24,7 @@ export default function unsavedChildren( allowDeepUnsaved?: boolean ): Array { const encountered = { - objects: {}, + objects: {} as Record, files: [], }; const identifier = obj.className + ':' + obj._getId(); @@ -35,17 +35,17 @@ export default function unsavedChildren( traverse(attributes[attr], encountered, false, !!allowDeepUnsaved); } } - const unsaved = []; + const unsaved: ParseObject[] = []; for (const id in encountered.objects) { if (id !== identifier && encountered.objects[id] !== true) { - unsaved.push(encountered.objects[id]); + unsaved.push(encountered.objects[id] as ParseObject); } } return unsaved.concat(encountered.files); } function traverse( - obj: ParseObject, + obj: ParseObject | ParseFile | ParseRelation | Array, encountered: EncounterMap, shouldThrow: boolean, allowDeepUnsaved: boolean diff --git a/src/uuid.js b/src/uuid.ts similarity index 68% rename from src/uuid.js rename to src/uuid.ts index 450d4976c..8308a37e6 100644 --- a/src/uuid.js +++ b/src/uuid.ts @@ -1,8 +1,8 @@ -let uuid = null; +let uuid: () => string = null as any; if (process.env.PARSE_BUILD === 'weapp') { uuid = function () { - const s = []; + const s: string[] = []; const hexDigits = '0123456789abcdef'; for (let i = 0; i < 36; i++) { @@ -10,9 +10,8 @@ if (process.env.PARSE_BUILD === 'weapp') { } s[14] = '4'; // bits 12-15 of the time_hi_and_version field to 0010 - s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01 + s[19] = hexDigits.substr((Number(s[19]) & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01 s[8] = s[13] = s[18] = s[23] = '-'; - return s.join(''); }; } else { @@ -21,3 +20,4 @@ if (process.env.PARSE_BUILD === 'weapp') { } module.exports = uuid; +export default uuid; diff --git a/tsconfig.json b/tsconfig.json index 051477c29..b5166cc62 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,8 +8,7 @@ "noImplicitAny": false, "allowJs": false }, - "files": [ - "src/Parse.ts", - "src/ParseSession.ts" + "include": [ + "src/*.ts" ] }