diff --git a/.eslintrc b/.eslintrc index fef5ebc..2b60d3a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -13,5 +13,8 @@ "no-var": "error", "prefer-const": "error" }, - "overrides": [{ "files": ["test/**/*.js"], "parserOptions": { "ecmaVersion": 2018 } }] + "overrides": [ + { "files": ["test/**/*.js"], "parserOptions": { "ecmaVersion": 2018 } }, + { "files": ["index.js.flow"], "parser": "babel-eslint", "rules": { "no-unused-vars": "off" } } + ] } diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..eb6b4b7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.js.flow linguist-language=js diff --git a/index.js.flow b/index.js.flow new file mode 100644 index 0000000..da3d7a5 --- /dev/null +++ b/index.js.flow @@ -0,0 +1,644 @@ +/** + * @flow strict + * + * Type definitions for mongoist in Flow. + */ + +type ReadPreference = + | 'primary' + | 'primaryPreferred' + | 'secondary' + | 'secondaryPreferred' + | 'nearest' + | null; + +type WriteConcern = {| + w?: number | 'majority' | string | null, + wtimeout?: number | null, + j?: boolean, +|}; + +declare class Timestamp { + static MAX_VALUE: Timestamp; + static MIN_VALUE: Timestamp; + static NEG_ONE: Timestamp; + static ONE: Timestamp; + static ZERO: Timestamp; + + static fromBits(low: number, high: number): Timestamp; + static fromInt(value: number): Timestamp; + static fromNumber(value: number): Timestamp; + static fromString(value: string, radix?: number): Timestamp; + + constructor(low: number, high: number): Timestamp; + + add(Timestamp): Timestamp; + and(Timestamp): Timestamp; + compare(Timestamp): number; + div(Timestamp): Timestamp; + equals(Timestamp): boolean; + getHighBits(): number; + getLowBits(): number; + getLowBitsUnsigned(): number; + getNumBitsAbs(): number; + greaterThan(Timestamp): boolean; + greaterThanOrEqual(Timestamp): boolean; + isNegative(): boolean; + isOdd(): boolean; + isZero(): boolean; + lessThan(Timestamp): boolean; + lessThanOrEqual(Timestamp): boolean; + modulo(Timestamp): Timestamp; + multiply(Timestamp): Timestamp; + negate(): Timestamp; + not(): Timestamp; + notEquals(Timestamp): boolean; + or(Timestamp): Timestamp; + shiftLeft(numBits: number): Timestamp; + shiftRight(numBits: number): Timestamp; + shiftRightUnsigned(numBits: number): Timestamp; + subtract(Timestamp): Timestamp; + toInt(): number; + toJSON(): string; + toNumber(): number; + toString(radix?: number): string; + xor(Timestamp): Timestamp; +} + +type TransactionOptions = {| + readConcern?: mixed, + writeConcern?: WriteConcern, + readPreference?: ReadPreference, +|}; + +// Not actually exported by mongoist, but used structurally in parameter +// types. +interface ClientSession { + id: mixed; + + abortTransaction(): Promise; + abortTransaction(callback: (Error | null, reply?: null) => mixed): void; + advanceOperationTime(Timestamp): void; + commitTransaction(): Promise; + commitTransaction(callback: (Error | null, reply?: null) => mixed): void; + endSession(): void; + endSession(callback: (Error | null, reply?: null) => mixed): void; + endSession( + options: ?{ [string]: mixed, ... }, + callback: (Error | null, reply?: null) => mixed + ): void; + equals(ClientSession): boolean; + incrementTransactionNumber(): void; + inTransaction(): boolean; + startTransaction(options?: TransactionOptions): void; + withTransaction( + fn: (session: ClientSession) => Promise, + options?: TransactionOptions + ): Promise; +} + +type BulkExecuteResult = {| + ok: 1, + writeErrors: mixed[], + writeConcernErrors: mixed[], + nInserted: number, + nUpserted: number, + nMatched: number, + nModified: number, + nRemoved: number, + upserted: mixed[], +|}; + +type BulkJSONValue = {| + nInsertOps: number, + nUpdateOps: number, + nRemoveOps: number, + nBatches: number, +|}; + +interface FindSyntax { + upsert(): FindSyntax; + remove(): void; + removeOne(): void; + update(update: mixed): void; + updateOne(update: mixed): void; + replaceOne(update: mixed): void; +} + +interface Bulk { + ensureCommand(cmdName: string, bulkCollection: mixed): mixed; + find(selector: Query): FindSyntax; + insert(doc: { [string]: mixed, ... }): void; + execute(): Promise; + pushCurrentCmd(): void; + tojson(): BulkJSONValue; +} + +type BulkOptions = {| + ...WriteConcern, + ignoreUndefined?: boolean, + session?: ClientSession, +|}; + +export type ConnectionOptions = {| + poolSize?: number, + ssl?: boolean, + sslValidate?: boolean, + sslCA?: (Buffer | string)[], + sslCert?: Buffer | string, + sslKey?: Buffer | string, + sslPass?: Buffer | string, + sslCRL?: Buffer, + autoReconnect?: boolean, + noDelay?: boolean, + keepAlive?: number, + keepAliveInitialDelay?: number, + connectTimeoutMS?: number, + family?: 4 | 6 | null, + socketTimeoutMS?: number, + reconnectTries?: number, + reconnectInterval?: number, + ha?: boolean, + haInterval?: number, + replicaSet?: string | null, + secondaryAcceptableLatencyMS?: number, + acceptableLatencyMS?: number, + connectWithNoPrimary?: boolean, + authSource?: string | null, + ...WriteConcern, + forceServerObjectId?: boolean, + serializeFunctions?: boolean, + ignoreUndefined?: boolean, + raw?: boolean, + bufferMaxEntries?: number, + promoteValues?: boolean, + promoteBuffers?: boolean, + promoteLongs?: boolean, + domainsEnabled?: boolean, + bufferMaxEntries?: number, + readPreference?: ReadPreference, + // Some sort of vaguely-defined generator for primary keys. + pkFactory?: mixed, + promiseLibrary?: mixed | null, + readConcern?: {| + level?: 'local' | 'available' | 'majority' | 'linearizable' | 'snapshot', + |}, + maxStalenessSeconds?: number | null, + appname?: string | null, + loggerLevel?: string | null, + // TODO: is this right? + logger?: typeof console | null, + checkServerIdentity?: boolean | mixed, + validateOptions?: mixed, + auth?: {| + user?: string, + password?: string, + |}, + authMechanism?: 'MDEFAULT' | 'GSSAPI' | 'PLAIN' | 'MONGODB-X509' | 'SCRAM-SHA-1', + compression?: mixed, + fsync?: boolean, + readPreferenceTags?: mixed[], + numberOfRetries?: number, + auto_reconnect?: boolean, + minSize?: number, +|}; + +/* eslint-disable no-undef */ +declare opaque type Connection; +declare opaque type InternalCollection; +declare opaque type InternalCursor; +/* eslint-enable no-undef */ + +type Query = { + [string]: mixed, + ..., +}; + +type CollationParam = {| + locale: string, + caseLevel?: boolean, + caseFirst?: 'upper' | 'lower' | 'off', + strength?: 1 | 2 | 3 | 4 | 5, + numericOrdering?: boolean, + alternate?: 'non-ignorable' | 'shifted', + maxVariable?: 'punct' | 'space', + backwards?: boolean, + normalization?: boolean, +|}; + +type TailableOptions = + | {| + tailable?: false, + |} + | {| + tailable: true, + awaitData?: false, + |} + | {| + tailable: true, + awaitData: true, + maxAwaitTimeMS?: number, + |}; + +type QueryOptions = {| + limit?: number, + sort?: mixed, + projection?: Projection, + skip?: number, + hint?: mixed, + explain?: boolean, + snapshot?: boolean, + timeout?: boolean, + ...TailableOptions, + batchSize?: number, + returnKey?: boolean, + min?: number, + max?: number, + showDiskLoc?: boolean, + comment?: string, + raw?: boolean, + promoteLongs?: boolean, + promoteValues?: boolean, + promoteBuffers?: boolean, + readPreference?: ReadPreference, + partial?: boolean, + maxTimeMS?: number, + noCursorTimeout?: boolean, + collation?: CollationParam, + session?: ClientSession, +|}; + +type FindAndModifyParams = {| + query: Query, + sort?: mixed, + remove?: boolean, + update?: mixed[] | { [string]: mixed, ... }, + new?: boolean, + fields?: Projection, + upsert?: boolean, + bypassDocumentValidation?: boolean, + writeConcern?: WriteConcern, + maxTimeMS?: number, + findAndModify?: string, + collation?: CollationParam, + // mutually exclusive with update: mixed[] + arrayFilters?: mixed[], +|}; + +type BaseUpsertOptions = {| + ...WriteConcern, + bypassDocumentValidation?: boolean, + checkKeys?: boolean, + ignoreUndefined?: boolean, + serializeFunctions?: boolean, + // TODO: merge with WriteConcern + session?: ClientSession, +|}; + +type UpdateOptions = {| + ...BaseUpsertOptions, + arrayFilters?: mixed[], + collation?: CollationParam, + hint?: mixed, + upsert?: boolean, + multi?: boolean, +|}; + +type ReplaceOptions = {| + ...BaseUpsertOptions, + collation?: CollationParam, + hint?: mixed, +|}; + +type InsertOptions = {| + ...BaseUpsertOptions, + forceServerObjectId?: boolean, +|}; + +type RemoveOptions = + | boolean + | {| + ...WriteConcern, + justOne: boolean, + collation?: CollationParam, + checkKeys?: boolean, + serializeFunctions?: boolean, + ignoreUndefined?: boolean, + session?: ClientSession, + |}; + +type RenameOptions = {| + dropTarget?: boolean, + session?: ClientSession, +|}; + +type UpdateOrReplaceResult = {| + ok: number, + n: number, + nModified: number, +|}; + +// Note that this type differs slightly from the one provided by the MongoDB native driver. +type DeleteResult = {| + // ok === 1 on success + ok: number, + n: number, + deletedCount: number, +|}; + +type BaseRunCommandOptions = {| + readPreference?: ReadPreference, +|}; + +type RunCommandOptions = {| + [string]: 1, + ...BaseRunCommandOptions, +|}; + +interface MongoJSDuck { + _getConnection(any): mixed; +} + +type MapParam = string | (() => void); +type ReduceParam = string | ((key: mixed, values: mixed[]) => R); + +// http://mongodb.github.io/node-mongodb-native/3.3/api/Collection.html#mapReduce +type MapReduceOptions = {| + readPreference?: ReadPreference, + out: {| inline: 1 |} | {| ['replace' | 'merge' | 'reduce']: 'string' |}, + query?: Query, + sort?: mixed, + limit?: number, + keeptemp?: boolean, + finalize?: string | ((key: mixed, value: R) => mixed), + scope?: mixed, + jsMode?: boolean, + verbose?: boolean, + bypassDocumentValidation?: boolean, + session?: ClientSession, +|}; + +type Index = string | mixed[] | { [string]: mixed, ... }; +type IndexOptions = {| + ...WriteConcern, + unique?: boolean, + sparse?: boolean, + background?: boolean, + dropDups?: boolean, + min?: number, + max?: number, + v?: number, + expireAfterSeconds?: number, + name?: string, + partialFilterExpression?: { [string]: mixed, ... }, + collation?: CollationParam, + session?: ClientSession, +|}; + +type AggregationPipeline = mixed[]; +type AggregationOptions = {| + readPreference?: ReadPreference, + batchSize?: number, + cursor?: mixed, + explain?: boolean, + allowDiskUse?: boolean, + maxTimeMS?: number, + maxAwaitTimeMS?: number, + bypassDocumentValidation?: boolean, + raw?: boolean, + promoteLongs?: boolean, + promoteValues?: boolean, + promoteBuffers?: boolean, + collation?: CollationParam, + comment?: string, + hint?: string | { [string]: mixed, ... }, + session?: ClientSession, +|}; + +// https://github.com/facebook/flow/issues/2753 +declare class Cursor extends stream$Readable { + constructor(cursorFactory: () => Promise): Cursor; + + map(fn: (doc: Doc) => V): Promise; + forEach(fn: (doc: Doc) => mixed): Promise; + count(): Promise; + size(): Promise; + explain(): Promise; + destroy(): Promise; + close(): Promise; + next(): Promise; + hasNext(): Promise; + rewind(): Promise; + toArray(): Promise; + addCursorFlag(flag: string, value: mixed): this; + getCursor(): Promise; + + @@asyncIterator(): AsyncIterator; +} + +type Expression = mixed; + +type ConstrainedProjection = { + [string]: + | V + | {| $meta: 'textScore' |} + | {| $slice: number | [number, number] |} + | {| $elemMatch: Expression |}, + ..., +}; + +export type Projection = { + ...ConstrainedProjection<0 | false> | ConstrainedProjection<1 | true>, + _id?: false | 0 | true | 1, + ... +}; + +type AggregationUpdate = ( + | {| + ['$addFields' | '$set' | '$replaceWith']: { [string]: Expression, ... }, + |} + | {| + $project: + | { [string]: 0 | false, _id?: Expression | 0 | false | 1 | true, ... } + | { [string]: Expression | 1 | true, _id?: Expression | 0 | false | 1 | true, ... }, + |} + | {| $unset: string | string[] |} + | {| $replaceRoot: { newRoot: { [string]: Expression, ... } } |} +)[]; + +type AtomicUpdate = { + [ + | '$addToSet' + | '$bit' + | '$currentDate' + | '$inc' + | '$max' + | '$min' + | '$mul' + | '$pop' + | '$pull' + | '$pullAll' + | '$push' + | '$rename' + | '$set' + | '$setOnInsert' + | '$unset']: mixed, + ..., +}; + +type AddObjectIDField = (Doc) => { _id: ObjectID, ...Doc }; + +declare class Collection { + constructor(db: Database_, name: string): Collection; + + connect(): Promise; + find(query: Query, projection?: Projection, options?: QueryOptions): Promise; + findAsCursor( + query: Query, + projection?: Projection, + options?: QueryOptions + ): Promise>; + findOne(query: Query, projection?: Projection, options?: QueryOptions): Promise; + findAndModify(options: FindAndModifyParams): Promise; + count(query: Query): Promise; + distinct(field: string, query: Query): Promise; + insert: typeof Collection.prototype.insertOne & typeof Collection.prototype.insertMany; + insertOne(doc: Doc, options?: InsertOptions): Promise<$Call>; + insertMany(docs: Docs, options?: InsertOptions): Promise<$TupleMap>; + // TODO: improve safety by restricting update type. + update( + query: Query, + update: AtomicUpdate | AggregationUpdate, + options?: UpdateOptions + ): Promise; + replaceOne( + query: Query, + replacement: mixed, + options?: ReplaceOptions + ): Promise; + // TODO: restrict to some form of ArbitraryBSON. + save(doc: Doc, options?: BaseUpsertOptions): Promise<{| _id: ObjectID, ...Doc |}>; + remove(query: Query, options?: RemoveOptions): Promise; + rename(name: string, options?: RenameOptions): Promise; + // TODO: refine this based on the provided command name. + runCommand(command: string, options?: BaseRunCommandOptions): Promise; + drop(): Promise; + stats(): Promise; + mapReduce(map: MapParam, reduce: ReduceParam, options: MapReduceOptions): Promise; + dropIndexes(): Promise; + dropIndex(index: string): Promise; + createIndex(index: Index, options?: IndexOptions): Promise; + ensureIndex: typeof Collection.prototype.createIndex; + getIndexes(): Promise; + reIndex(): Promise; + isCapped(): Promise; + aggregate(pipeline: AggregationPipeline, options?: AggregationOptions): Promise; + aggregateAsCursor(pipeline: AggregationPipeline, options?: AggregationOptions): Promise; + initializeOrderedBulkOp(options?: BulkOptions): Bulk; + initializeUnorderedBulkOp(options?: BulkOptions): Bulk; +} + +type MaybePromise = T | Promise; + +type CollectionOptions = {| + capped?: boolean, + size?: number, + max?: number, + storageEngine?: { [string]: mixed, ... }, + validator?: { [string]: mixed, ... }, + validationLevel?: 'off' | 'strict' | 'moderate', + validationAction?: 'error' | 'warn', + indexOptionDefaults?: { [string]: mixed, ... }, + collation?: CollationParam, + writeConcern?: { [string]: mixed, ... }, +|}; + +type Stats = {| + db: string, + collections: number, + objects: number, + avgObjectSize: number, + dataSize: number, + storageSize: number, + numExtents: number, + indexes: number, + indexSize: number, + scaleFactor?: number, + fsUsedSize?: number, + fsTotalSize?: number, + ok?: number, +|}; + +// https://github.com/facebook/flow/issues/2753 +declare class Database_ extends events$EventEmitter { + constructor(connectionString: MaybePromise, options?: ConnectionOptions): Database_; + constructor(database: MaybePromise): Database_; + + connect(): Promise; + close(force: boolean): Promise; + dropDatabase(): Promise; + + collection(name: string): Collection; + createCollection(name: string, options?: CollectionOptions): Promise; + getCollectionInfos(): Promise; + getCollectionNames(): Promise; + listCollections(): Promise; + + adminCommand(command: string | RunCommandOptions): Promise; + runCommand(command: string | RunCommandOptions): Promise; + + createUser(user: mixed): Promise; + dropAllUsers(): Promise; + dropUser(username: string): Promise; + + getLastError(): Promise; + getLastErrorObj(): Promise<{ [string]: mixed, ... }>; + stats(scale: mixed): Promise; +} + +type DatabaseWithProxy = Database_ & { [string]: Collection, ... }; + +declare export class ObjectID { + constructor(value?: ?(string | number | ObjectID)): ObjectID; + + generationTime: number; + + static createFromHexString(hexString: string): ObjectID; + static createFromTime(time: number): ObjectID; + static isValid(id: string | number | ObjectID): boolean; + + equals(ObjectID): boolean; + generate(time?: number): Buffer; + getTimestamp(): Date; + toHexString(): string; +} + +type ConnectFn = {| + (connectionParam: MaybePromise, options?: ConnectionOptions): DatabaseWithProxy, + (connectionParam: MaybePromise): DatabaseWithProxy, + + default: ConnectFn, + + Database: DatabaseWithProxy, + Collection: typeof Collection, + Cursor: typeof Cursor, + Bulk: Bulk, + ObjectId: typeof ObjectID, + ObjectID: typeof ObjectID, + + Binary: mixed, + Code: mixed, + DBRef: mixed, + Double: mixed, + Long: mixed, + NumberLong: mixed, + MinKey: mixed, + MaxKey: mixed, + Symbol: mixed, + Timestamp: typeof Timestamp, +|}; + +declare export default ConnectFn; + +export const ObjectId = ObjectID; +export type Database = DatabaseWithProxy; diff --git a/package-lock.json b/package-lock.json index f00a086..7d0815d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mongoist", - "version": "2.2.0", + "version": "2.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2059,6 +2059,20 @@ "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", "dev": true }, + "babel-eslint": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.0.3.tgz", + "integrity": "sha512-z3U7eMY6r/3f3/JB9mTsLjyxrv0Yb1zb8PCWCLpguxfCzBIZUwy23R1t/XKewP+8mEN2Ck8Dtr4q20z6ce6SoA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.0.0", + "@babel/traverse": "^7.0.0", + "@babel/types": "^7.0.0", + "eslint-visitor-keys": "^1.0.0", + "resolve": "^1.12.0" + } + }, "babel-plugin-dynamic-import-node": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz", @@ -4961,7 +4975,7 @@ }, "find-up": { "version": "3.0.0", - "resolved": false, + "resolved": "", "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "dev": true, "requires": { @@ -5000,7 +5014,7 @@ }, "locate-path": { "version": "3.0.0", - "resolved": false, + "resolved": "", "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "dev": true, "requires": { @@ -5019,7 +5033,7 @@ }, "p-locate": { "version": "3.0.0", - "resolved": false, + "resolved": "", "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "dev": true, "requires": { @@ -5040,7 +5054,7 @@ }, "resolve-from": { "version": "4.0.0", - "resolved": false, + "resolved": "", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, diff --git a/package.json b/package.json index f496760..63b7dc8 100644 --- a/package.json +++ b/package.json @@ -3,14 +3,19 @@ "version": "2.4.0", "description": "Mongodb driver inspired by mongojs built with async/await in mind", "main": "index.js", + "files": [ + "index.js", + "index.js.flow", + "lib" + ], "engines": { "node": ">=6" }, "scripts": { "test": "nyc --reporter=text-summary mocha --recursive --require @babel/polyfill --require @babel/register", "coverage": "nyc report --reporter=text-lcov | coveralls", - "lint": "eslint .", - "lint:fix": "eslint . --fix", + "lint": "eslint . --ext '.js, .js.flow'", + "lint:fix": "npm run lint -- --fix", "release": "standard-version" }, "repository": { @@ -40,6 +45,7 @@ "@babel/polyfill": "^7.7.0", "@babel/preset-env": "^7.7.1", "@babel/register": "^7.7.0", + "babel-eslint": "^10.0.3", "chai": "^4.2.0", "coveralls": "^3.0.7", "drop-mongodb-collections": "^1.2.5",