Skip to content

Commit cfbafad

Browse files
committed
[Do Not Merge] PoC for shell-api autocomplete type definitions
```sh cd packages/shell-api && \ npm run compile && \ npx api-extractor run ; \ npx ts-node bin/api-postprocess.ts ; \ cat lib/api-processed.d.ts ```
1 parent 9ebc147 commit cfbafad

File tree

12 files changed

+768
-81
lines changed

12 files changed

+768
-81
lines changed

package-lock.json

Lines changed: 468 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
3+
"mainEntryPointFilePath": "<projectFolder>/lib/index.d.ts",
4+
"apiReport": {
5+
"enabled": false
6+
},
7+
"docModel": {
8+
"enabled": false
9+
},
10+
"bundledPackages": [
11+
"@mongosh/service-provider-core",
12+
"@mongosh/types",
13+
"mongodb",
14+
"bson"
15+
],
16+
"dtsRollup": {
17+
"enabled": true,
18+
"untrimmedFilePath": "",
19+
"publicTrimmedFilePath": "<projectFolder>/lib/api-raw.d.ts"
20+
},
21+
"tsdocMetadata": {
22+
"enabled": false
23+
},
24+
"newlineKind": "lf",
25+
"messages": {
26+
"compilerMessageReporting": {
27+
"default": {
28+
"logLevel": "error"
29+
}
30+
},
31+
"extractorMessageReporting": {
32+
"default": {
33+
"logLevel": "error"
34+
},
35+
"ae-internal-missing-underscore": {
36+
"logLevel": "none",
37+
"addToApiReportFile": false
38+
},
39+
"ae-forgotten-export": {
40+
"logLevel": "error",
41+
"addToApiReportFile": false
42+
}
43+
},
44+
"tsdocMessageReporting": {
45+
"default": {
46+
"logLevel": "none"
47+
}
48+
}
49+
}
50+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import * as babel from '@babel/core';
2+
import type * as BabelTypes from '@babel/types';
3+
import { promises as fs } from 'fs';
4+
import path from 'path';
5+
import { signatures } from '../';
6+
7+
function applyAsyncRewriterChanges() {
8+
return ({
9+
types: t,
10+
}: {
11+
types: typeof BabelTypes;
12+
}): babel.PluginObj<{
13+
processedMethods: [string, string, BabelTypes.ClassBody][];
14+
}> => {
15+
return {
16+
pre() {
17+
this.processedMethods = [];
18+
},
19+
post() {
20+
for (const className of Object.keys(signatures)) {
21+
for (const methodName of Object.keys(
22+
signatures[className].attributes ?? {}
23+
)) {
24+
if (
25+
signatures[className].attributes?.[methodName].returnsPromise &&
26+
!signatures[className].attributes?.[methodName].inherited
27+
) {
28+
if (
29+
!this.processedMethods.find(
30+
([cls, method]) => cls === className && method === methodName
31+
)
32+
) {
33+
console.error(
34+
`Expected to find and transpile type for @returnsPromise-annotated method ${className}.${methodName}`
35+
);
36+
}
37+
}
38+
}
39+
}
40+
},
41+
visitor: {
42+
TSDeclareMethod(path) {
43+
if ('isMongoshAsyncRewrittenMethod' in path.node) return;
44+
45+
if (path.parent.type !== 'ClassBody') return;
46+
if (path.parentPath.parent.type !== 'ClassDeclaration') return;
47+
const classId = path.parentPath.parent.id;
48+
if (classId?.type !== 'Identifier') return;
49+
const className = classId.name;
50+
if (path.node.key.type !== 'Identifier') return;
51+
const methodName = path.node.key.name;
52+
53+
if (
54+
this.processedMethods.find(
55+
([cls, method, classBody]) =>
56+
cls === className &&
57+
method === methodName &&
58+
classBody !== path.parent
59+
)
60+
) {
61+
throw new Error(`Duplicate method: ${className}.${methodName}`);
62+
}
63+
this.processedMethods.push([className, methodName, path.parent]);
64+
65+
if (!signatures[className]?.attributes?.[methodName]?.returnsPromise)
66+
return;
67+
68+
const { returnType } = path.node;
69+
if (returnType?.type !== 'TSTypeAnnotation') return;
70+
if (returnType.typeAnnotation.type !== 'TSTypeReference') return;
71+
if (returnType.typeAnnotation.typeName.type !== 'Identifier') return;
72+
if (returnType.typeAnnotation.typeName.name !== 'Promise') return;
73+
if (!returnType.typeAnnotation.typeParameters?.params.length) return;
74+
path.replaceWith({
75+
...path.node,
76+
returnType: {
77+
...returnType,
78+
typeAnnotation:
79+
returnType.typeAnnotation.typeParameters.params[0],
80+
},
81+
isMongoshAsyncRewrittenMethod: true,
82+
});
83+
},
84+
},
85+
};
86+
};
87+
}
88+
89+
async function main() {
90+
const apiRaw = await fs.readFile(
91+
path.resolve(__dirname, '..', 'lib', 'api-raw.d.ts'),
92+
'utf8'
93+
);
94+
const result = babel.transformSync(apiRaw, {
95+
code: true,
96+
ast: false,
97+
configFile: false,
98+
babelrc: false,
99+
browserslistConfigFile: false,
100+
compact: false,
101+
sourceType: 'module',
102+
plugins: [applyAsyncRewriterChanges()],
103+
parserOpts: {
104+
plugins: ['typescript'],
105+
},
106+
});
107+
let code = result?.code ?? '';
108+
code += `
109+
// REPLACEME
110+
type MongodbServerSchema = {
111+
admin: {},
112+
config: {},
113+
test: {
114+
test: {
115+
schema: {
116+
_id: ObjectId;
117+
foo: number;
118+
}
119+
}
120+
}
121+
}
122+
// REPLACEME
123+
124+
declare global {
125+
// second argument optional
126+
var db: Database<MongodbServerSchema, MongodbServerSchema['test']>;
127+
128+
var use: (collection: StringKey<MongodbServerSchema>) => void;
129+
}
130+
`;
131+
await fs.writeFile(
132+
path.resolve(__dirname, '..', 'lib', 'api-processed.d.ts'),
133+
code
134+
);
135+
}
136+
137+
main().catch((err) =>
138+
process.nextTick(() => {
139+
throw err;
140+
})
141+
);

packages/shell-api/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
},
1414
"scripts": {
1515
"compile": "tsc -p tsconfig.json",
16+
"api-generate": "api-extractor run ; ts-node bin/api-postprocess.ts",
1617
"pretest": "npm run compile",
1718
"eslint": "eslint",
1819
"lint": "npm run eslint . && npm run prettier -- --check .",
@@ -48,6 +49,7 @@
4849
"mongodb-redact": "^0.2.2"
4950
},
5051
"devDependencies": {
52+
"@microsoft/api-extractor": "^7.39.3",
5153
"@mongodb-js/eslint-config-mongosh": "^1.0.0",
5254
"@mongodb-js/prettier-config-devtools": "^1.0.1",
5355
"@mongodb-js/tsconfig-mongosh": "^1.0.0",

packages/shell-api/src/collection.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ import type {
2222
FindAndModifyMethodShellOptions,
2323
RemoveShellOptions,
2424
MapReduceShellOptions,
25+
GenericCollectionSchema,
26+
GenericDatabaseSchema,
27+
GenericServerSideSchema,
28+
StringKey,
2529
} from './helpers';
2630
import {
2731
adaptAggregateOptions,
@@ -90,11 +94,15 @@ import { ShellApiErrors } from './error-codes';
9094

9195
@shellApiClassDefault
9296
@addSourceToResults
93-
export default class Collection extends ShellApiWithMongoClass {
94-
_mongo: Mongo;
95-
_database: Database;
96-
_name: string;
97-
constructor(mongo: Mongo, database: Database, name: string) {
97+
export class Collection<
98+
M extends GenericServerSideSchema = GenericServerSideSchema,
99+
D extends GenericDatabaseSchema = M[keyof M],
100+
C extends GenericCollectionSchema = D[keyof D]
101+
> extends ShellApiWithMongoClass {
102+
_mongo: Mongo<M>;
103+
_database: Database<M, D>;
104+
_name: StringKey<D>;
105+
constructor(mongo: Mongo<M>, database: Database<M, D>, name: StringKey<D>) {
98106
super();
99107
this._mongo = mongo;
100108
this._database = database;
@@ -513,7 +521,7 @@ export default class Collection extends ShellApiWithMongoClass {
513521
query: Document = {},
514522
projection?: Document,
515523
options: FindOptions = {}
516-
): Promise<Document | null> {
524+
): Promise<C['schema'] | null> {
517525
if (projection) {
518526
options.projection = projection;
519527
}
@@ -1406,7 +1414,7 @@ export default class Collection extends ShellApiWithMongoClass {
14061414
* @return {Database}
14071415
*/
14081416
@returnType('Database')
1409-
getDB(): Database {
1417+
getDB(): Database<M, D> {
14101418
this._emitCollectionApiCall('getDB');
14111419
return this._database;
14121420
}
@@ -1417,7 +1425,7 @@ export default class Collection extends ShellApiWithMongoClass {
14171425
* @return {Mongo}
14181426
*/
14191427
@returnType('Mongo')
1420-
getMongo(): Mongo {
1428+
getMongo(): Mongo<M> {
14211429
this._emitCollectionApiCall('getMongo');
14221430
return this._mongo;
14231431
}
@@ -1754,7 +1762,7 @@ export default class Collection extends ShellApiWithMongoClass {
17541762
}
17551763

17561764
const ns = `${this._database._name}.${this._name}`;
1757-
const config = this._mongo.getDB('config');
1765+
const config = this._mongo.getDB('config' as StringKey<M>);
17581766
if (collStats[0].shard) {
17591767
result.shards = shardStats;
17601768
}
@@ -2061,7 +2069,7 @@ export default class Collection extends ShellApiWithMongoClass {
20612069
this._emitCollectionApiCall('getShardDistribution', {});
20622070

20632071
const result = {} as Document;
2064-
const config = this._mongo.getDB('config');
2072+
const config = this._mongo.getDB('config' as StringKey<M>);
20652073
const ns = `${this._database._name}.${this._name}`;
20662074

20672075
const configCollectionsInfo = await config
@@ -2396,3 +2404,5 @@ export default class Collection extends ShellApiWithMongoClass {
23962404
);
23972405
}
23982406
}
2407+
2408+
export default Collection;

packages/shell-api/src/database.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ import {
1111
ShellApiWithMongoClass,
1212
} from './decorators';
1313
import { asPrintable, ServerVersions, Topologies } from './enums';
14+
import type {
15+
GenericDatabaseSchema,
16+
GenericServerSideSchema,
17+
StringKey,
18+
} from './helpers';
1419
import {
1520
adaptAggregateOptions,
1621
adaptOptions,
@@ -66,15 +71,18 @@ type AuthDoc = {
6671
};
6772

6873
@shellApiClassDefault
69-
export default class Database extends ShellApiWithMongoClass {
70-
_mongo: Mongo;
71-
_name: string;
74+
export default class Database<
75+
M extends GenericServerSideSchema = GenericServerSideSchema,
76+
D extends GenericDatabaseSchema = M[keyof M]
77+
> extends ShellApiWithMongoClass {
78+
_mongo: Mongo<M>;
79+
_name: StringKey<M>;
7280
_collections: Record<string, Collection>;
7381
_session: Session | undefined;
74-
_cachedCollectionNames: string[] = [];
82+
_cachedCollectionNames: StringKey<D>[] = [];
7583
_cachedHello: Document | null = null;
7684

77-
constructor(mongo: Mongo, name: string, session?: Session) {
85+
constructor(mongo: Mongo<M>, name: StringKey<M>, session?: Session) {
7886
super();
7987
this._mongo = mongo;
8088
this._name = name;
@@ -308,11 +316,11 @@ export default class Database extends ShellApiWithMongoClass {
308316
}
309317

310318
@returnType('Mongo')
311-
getMongo(): Mongo {
319+
getMongo(): Mongo<M> {
312320
return this._mongo;
313321
}
314322

315-
getName(): string {
323+
getName(): StringKey<M> {
316324
return this._name;
317325
}
318326

@@ -323,9 +331,9 @@ export default class Database extends ShellApiWithMongoClass {
323331
*/
324332
@returnsPromise
325333
@apiVersions([1])
326-
async getCollectionNames(): Promise<string[]> {
334+
async getCollectionNames(): Promise<StringKey<D>[]> {
327335
this._emitDatabaseApiCall('getCollectionNames');
328-
return this._getCollectionNames();
336+
return (await this._getCollectionNames()) as StringKey<D>[];
329337
}
330338

331339
/**
@@ -437,7 +445,7 @@ export default class Database extends ShellApiWithMongoClass {
437445
}
438446

439447
@returnType('Database')
440-
getSiblingDB(db: string): Database {
448+
getSiblingDB<K extends StringKey<M>>(db: K): Database<M, M[K]> {
441449
assertArgsDefinedType([db], ['string'], 'Database.getSiblingDB');
442450
this._emitDatabaseApiCall('getSiblingDB', { db });
443451
if (this._session) {
@@ -447,7 +455,7 @@ export default class Database extends ShellApiWithMongoClass {
447455
}
448456

449457
@returnType('Collection')
450-
getCollection(coll: string): Collection {
458+
getCollection<K extends StringKey<D>>(coll: K): Collection<M, D, D[K]> {
451459
assertArgsDefinedType([coll], ['string'], 'Database.getColl');
452460
this._emitDatabaseApiCall('getCollection', { coll });
453461
if (!isValidCollectionName(coll)) {

packages/shell-api/src/decorators.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@ export interface TypeSignature {
381381
isDirectShellCommand?: boolean;
382382
acceptsRawInput?: boolean;
383383
shellCommandCompleter?: ShellCommandCompleter;
384+
inherited?: boolean;
384385
}
385386

386387
/**
@@ -426,6 +427,7 @@ type ClassSignature = {
426427
isDirectShellCommand: boolean;
427428
acceptsRawInput?: boolean;
428429
shellCommandCompleter?: ShellCommandCompleter;
430+
inherited?: true;
429431
};
430432
};
431433
};
@@ -574,6 +576,7 @@ function shellApiClassGeneric<T extends { prototype: any }>(
574576
isDirectShellCommand: method.isDirectShellCommand,
575577
acceptsRawInput: method.acceptsRawInput,
576578
shellCommandCompleter: method.shellCommandCompleter,
579+
inherited: true,
577580
};
578581

579582
const attributeHelpKeyPrefix = `${superClassHelpKeyPrefix}.attributes.${propertyName}`;

0 commit comments

Comments
 (0)