@typeorm/codemod 1.0.1 → 1.0.3-nightly.20261009
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +2 -1
- package/dist/cli/print-usage.js +1 -0
- package/dist/cli/print-usage.js.map +1 -1
- package/dist/cli/run-transforms.js +167 -79
- package/dist/cli/run-transforms.js.map +1 -1
- package/dist/dependencies/v1.js +1 -0
- package/dist/dependencies/v1.js.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/transforms/ast-helpers.js +820 -26
- package/dist/transforms/ast-helpers.js.map +1 -1
- package/dist/transforms/stats.js +11 -0
- package/dist/transforms/stats.js.map +1 -1
- package/dist/transforms/todo.js +22 -5
- package/dist/transforms/todo.js.map +1 -1
- package/dist/transforms/v1/column-readonly.js +28 -8
- package/dist/transforms/v1/column-readonly.js.map +1 -1
- package/dist/transforms/v1/column-unsigned-numeric.js +21 -18
- package/dist/transforms/v1/column-unsigned-numeric.js.map +1 -1
- package/dist/transforms/v1/column-width-zerofill.js +9 -3
- package/dist/transforms/v1/column-width-zerofill.js.map +1 -1
- package/dist/transforms/v1/connection-options-reader.js +23 -7
- package/dist/transforms/v1/connection-options-reader.js.map +1 -1
- package/dist/transforms/v1/connection-to-datasource.js +871 -88
- package/dist/transforms/v1/connection-to-datasource.js.map +1 -1
- package/dist/transforms/v1/datasource-expo.js +79 -0
- package/dist/transforms/v1/datasource-expo.js.map +1 -0
- package/dist/transforms/v1/datasource-mongodb.js +77 -50
- package/dist/transforms/v1/datasource-mongodb.js.map +1 -1
- package/dist/transforms/v1/datasource-mssql.js +88 -44
- package/dist/transforms/v1/datasource-mssql.js.map +1 -1
- package/dist/transforms/v1/datasource-mysql-connector.js +22 -6
- package/dist/transforms/v1/datasource-mysql-connector.js.map +1 -1
- package/dist/transforms/v1/datasource-name.js +17 -14
- package/dist/transforms/v1/datasource-name.js.map +1 -1
- package/dist/transforms/v1/datasource-sap.js +51 -39
- package/dist/transforms/v1/datasource-sap.js.map +1 -1
- package/dist/transforms/v1/datasource-sqlite-options.js +4 -1
- package/dist/transforms/v1/datasource-sqlite-options.js.map +1 -1
- package/dist/transforms/v1/file-logger.js +2 -21
- package/dist/transforms/v1/file-logger.js.map +1 -1
- package/dist/transforms/v1/find-options-string-relations.js +75 -11
- package/dist/transforms/v1/find-options-string-relations.js.map +1 -1
- package/dist/transforms/v1/find-options-string-select.js +75 -13
- package/dist/transforms/v1/find-options-string-select.js.map +1 -1
- package/dist/transforms/v1/global-functions.js +79 -21
- package/dist/transforms/v1/global-functions.js.map +1 -1
- package/dist/transforms/v1/index.js +6 -1
- package/dist/transforms/v1/index.js.map +1 -1
- package/dist/transforms/v1/mongodb-stats.js +18 -11
- package/dist/transforms/v1/mongodb-stats.js.map +1 -1
- package/dist/transforms/v1/mongodb-types.js +62 -2
- package/dist/transforms/v1/mongodb-types.js.map +1 -1
- package/dist/transforms/v1/query-builder-on-conflict.js +30 -17
- package/dist/transforms/v1/query-builder-on-conflict.js.map +1 -1
- package/dist/transforms/v1/query-builder-print-sql.js +15 -12
- package/dist/transforms/v1/query-builder-print-sql.js.map +1 -1
- package/dist/transforms/v1/query-builder-replace-property-names.js +13 -12
- package/dist/transforms/v1/query-builder-replace-property-names.js.map +1 -1
- package/dist/transforms/v1/query-builder-where-expression.js +29 -12
- package/dist/transforms/v1/query-builder-where-expression.js.map +1 -1
- package/dist/transforms/v1/query-runner-loaded-tables-views.js +0 -1
- package/dist/transforms/v1/query-runner-loaded-tables-views.js.map +1 -1
- package/dist/transforms/v1/relation-count.js +3 -0
- package/dist/transforms/v1/relation-count.js.map +1 -1
- package/dist/transforms/v1/repository-abstract.js +146 -29
- package/dist/transforms/v1/repository-abstract.js.map +1 -1
- package/dist/transforms/v1/repository-exist.js +18 -1
- package/dist/transforms/v1/repository-exist.js.map +1 -1
- package/dist/transforms/v1/repository-find-by-ids.js +67 -24
- package/dist/transforms/v1/repository-find-by-ids.js.map +1 -1
- package/dist/transforms/v1/repository-find-one-by-id.js +3 -0
- package/dist/transforms/v1/repository-find-one-by-id.js.map +1 -1
- package/dist/transforms/v1/use-container.js +60 -12
- package/dist/transforms/v1/use-container.js.map +1 -1
- package/package.json +16 -17
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.renameMemberMethod = exports.removeImportSpecifiers = exports.removeObjectProperties = exports.forEachDecoratorObjectArg = exports.expandLocalNamesForImports = exports.TYPEORM_COLUMN_DECORATORS = exports.forEachIdentifierParam = exports.getLocalNamesForImport = exports.fileImportsFrom = exports.isIdentifier = exports.setStringValue = exports.getStringValue = void 0;
|
|
3
|
+
exports.isRepositoryReceiver = exports.collectRepositoryBindings = exports.TYPEORM_DATASOURCE_TYPES = exports.TYPEORM_REPOSITORY_TYPES = exports.getTypeReferenceRootName = exports.renameMemberMethod = exports.renameReExportSpecifiers = exports.removeReExportSpecifiers = exports.removeImportSpecifiers = exports.removeObjectPropertiesWhere = exports.removeObjectProperties = exports.unwrapTsExpression = exports.isFindMethodCallArgument = exports.isObjectFromEntriesCall = exports.TYPEORM_FIND_OPTIONS_METHODS = exports.forEachColumnMetadataOptionsArg = exports.getObjectPropertyKeyName = exports.forEachDecoratorObjectArg = exports.expandLocalNamesForImports = exports.TYPEORM_COLUMN_DECORATORS = exports.forEachIdentifierParam = exports.getNamespaceLocalNames = exports.getLocalNamesForImport = exports.fileImportsFrom = exports.isIdentifier = exports.setStringValue = exports.getStringValue = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Extracts a string value from a StringLiteral or Literal node.
|
|
6
6
|
* Returns null if the node is not a string literal.
|
|
@@ -30,11 +30,12 @@ exports.setStringValue = setStringValue;
|
|
|
30
30
|
const isIdentifier = (node) => node.type === "Identifier";
|
|
31
31
|
exports.isIdentifier = isIdentifier;
|
|
32
32
|
/**
|
|
33
|
-
* Checks whether the file
|
|
34
|
-
*
|
|
35
|
-
* and recognizes ESM `import`,
|
|
36
|
-
*
|
|
37
|
-
*
|
|
33
|
+
* Checks whether the file references the given module as an import or a
|
|
34
|
+
* re-export source. Matches the exact module name (`"typeorm"`) and any
|
|
35
|
+
* sub-path (`"typeorm/..."`), and recognizes ESM `import`, ESM
|
|
36
|
+
* `export { X } from "..."` / `export * from "..."`, TypeScript
|
|
37
|
+
* `import = require(...)`, and CommonJS `require(...)` forms so barrel
|
|
38
|
+
* files and `.js`/`.jsx` callers still pass the scope guard.
|
|
38
39
|
*/
|
|
39
40
|
const fileImportsFrom = (root, j, moduleName) => {
|
|
40
41
|
const prefix = `${moduleName}/`;
|
|
@@ -46,6 +47,19 @@ const fileImportsFrom = (root, j, moduleName) => {
|
|
|
46
47
|
.some((path) => matchesModule(path.node.source.value))) {
|
|
47
48
|
return true;
|
|
48
49
|
}
|
|
50
|
+
// ESM: export { X } from "typeorm[/subpath]" / export * from "..."
|
|
51
|
+
// Barrel files that only re-export TypeORM APIs have no import at all,
|
|
52
|
+
// but their re-export source still counts as a reference to the module.
|
|
53
|
+
if (root
|
|
54
|
+
.find(j.ExportNamedDeclaration)
|
|
55
|
+
.some((path) => matchesModule(path.node.source?.value))) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
if (root
|
|
59
|
+
.find(j.ExportAllDeclaration)
|
|
60
|
+
.some((path) => matchesModule(path.node.source?.value))) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
49
63
|
// TS: import ... = require("typeorm[/subpath]")
|
|
50
64
|
if (root.find(j.TSImportEqualsDeclaration).some((path) => {
|
|
51
65
|
const ref = path.node.moduleReference;
|
|
@@ -73,31 +87,69 @@ exports.fileImportsFrom = fileImportsFrom;
|
|
|
73
87
|
* import { RelationCount as RC } from "typeorm" → { "RC" }
|
|
74
88
|
* const { RelationCount } = require("typeorm") → { "RelationCount" }
|
|
75
89
|
* const { RelationCount: RC } = require("typeorm")→ { "RC" }
|
|
90
|
+
*
|
|
91
|
+
* Matches both the exact module name and any sub-path — e.g. when moduleName
|
|
92
|
+
* is `"typeorm"`, imports from `"typeorm/metadata/ColumnMetadata"` are also
|
|
93
|
+
* recognised. TypeORM users reach internal types through deep imports, and
|
|
94
|
+
* the codemod must rewrite them regardless of import shape.
|
|
95
|
+
*
|
|
96
|
+
* CommonJS `require` destructures are collected from module scope only; an
|
|
97
|
+
* inside-function `require` would sit in an inner scope where the call-site
|
|
98
|
+
* shadow guards reject rewrites, so collecting it would silently mislead
|
|
99
|
+
* callers. See the inline comment on the CJS branch for details.
|
|
100
|
+
*
|
|
101
|
+
* @param opts.valueOnly When true, skip ESM type-only imports — both the
|
|
102
|
+
* declaration-level `import type { X }` and the per-specifier
|
|
103
|
+
* `import { type X }` form. Use this when the local is going to appear as
|
|
104
|
+
* a *value* in the rewritten code (e.g. callee of `new X(...)` or
|
|
105
|
+
* `X.method()`); a type-only import creates no runtime binding and must
|
|
106
|
+
* not influence value-level matching. CommonJS `require` destructures and
|
|
107
|
+
* TS `import = require` forms always produce value bindings so they are
|
|
108
|
+
* included regardless.
|
|
76
109
|
*/
|
|
77
|
-
const getLocalNamesForImport = (root, j, moduleName, importedName) => {
|
|
110
|
+
const getLocalNamesForImport = (root, j, moduleName, importedName, opts = {}) => {
|
|
78
111
|
const localNames = new Set();
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
112
|
+
const { valueOnly = false } = opts;
|
|
113
|
+
const prefix = `${moduleName}/`;
|
|
114
|
+
const matchesModule = (source) => typeof source === "string" &&
|
|
115
|
+
(source === moduleName || source.startsWith(prefix));
|
|
116
|
+
// ESM: `import { X [as Y] } from "moduleName[/subpath]"`
|
|
117
|
+
root.find(j.ImportDeclaration).forEach((importPath) => {
|
|
118
|
+
if (!matchesModule(importPath.node.source.value))
|
|
119
|
+
return;
|
|
120
|
+
const declKind = importPath.node.importKind;
|
|
121
|
+
if (valueOnly && declKind === "type")
|
|
122
|
+
return;
|
|
83
123
|
for (const spec of importPath.node.specifiers ?? []) {
|
|
84
124
|
if (spec.type !== "ImportSpecifier" ||
|
|
85
125
|
spec.imported.type !== "Identifier" ||
|
|
86
126
|
spec.imported.name !== importedName) {
|
|
87
127
|
continue;
|
|
88
128
|
}
|
|
129
|
+
const specKind = spec.importKind;
|
|
130
|
+
if (valueOnly && specKind === "type")
|
|
131
|
+
continue;
|
|
89
132
|
const local = spec.local ?? spec.imported;
|
|
90
133
|
if (local.type === "Identifier") {
|
|
91
134
|
localNames.add(local.name);
|
|
92
135
|
}
|
|
93
136
|
}
|
|
94
137
|
});
|
|
95
|
-
// CommonJS: `const { X [: Y] } = require("moduleName")`
|
|
138
|
+
// CommonJS: `const { X [: Y] } = require("moduleName[/subpath]")`
|
|
139
|
+
// Only top-level requires are collected. An inside-function require
|
|
140
|
+
// creates a binding at non-module scope, and the call-site scope guard
|
|
141
|
+
// in `forEachColumnMetadataOptionsArg` rejects callees whose declaration
|
|
142
|
+
// lives in any inner scope (shadow guard). Collecting inner-scope
|
|
143
|
+
// requires here would make the collector and the guard disagree — the
|
|
144
|
+
// name would be in `classLocalNames` but the rewrite would never fire.
|
|
145
|
+
// Users with inside-function requires can hoist them to module scope.
|
|
96
146
|
root.find(j.CallExpression, {
|
|
97
147
|
callee: { type: "Identifier", name: "require" },
|
|
98
148
|
}).forEach((callPath) => {
|
|
99
149
|
const [arg] = callPath.node.arguments;
|
|
100
|
-
if (!arg || (0, exports.getStringValue)(arg)
|
|
150
|
+
if (!arg || !matchesModule((0, exports.getStringValue)(arg)))
|
|
151
|
+
return;
|
|
152
|
+
if (callPath.scope?.isGlobal !== true)
|
|
101
153
|
return;
|
|
102
154
|
const parent = callPath.parent.node;
|
|
103
155
|
if (parent.type !== "VariableDeclarator")
|
|
@@ -132,6 +184,80 @@ const getLocalNamesForImport = (root, j, moduleName, importedName) => {
|
|
|
132
184
|
return localNames;
|
|
133
185
|
};
|
|
134
186
|
exports.getLocalNamesForImport = getLocalNamesForImport;
|
|
187
|
+
/**
|
|
188
|
+
* Collects local namespace bindings for a module. Covers:
|
|
189
|
+
*
|
|
190
|
+
* import * as typeorm from "typeorm" → "typeorm"
|
|
191
|
+
* const typeorm = require("typeorm") → "typeorm"
|
|
192
|
+
* import typeorm = require("typeorm") → "typeorm"
|
|
193
|
+
*
|
|
194
|
+
* Useful when a transform needs to recognise `typeorm.Foo` member-expression
|
|
195
|
+
* references alongside named `Foo` imports handled by `getLocalNamesForImport`.
|
|
196
|
+
*
|
|
197
|
+
* CJS namespace `require` bindings are collected from module scope only,
|
|
198
|
+
* matching the module-scope guard that `getLocalNamesForImport` applies for
|
|
199
|
+
* destructured requires — same reasoning, see the CJS branch comment there.
|
|
200
|
+
*
|
|
201
|
+
* @param opts.valueOnly When true, skip ESM type-only namespace imports
|
|
202
|
+
* (`import type * as ns from "..."`). Use this when the local name is
|
|
203
|
+
* going to appear as a *value* in the rewritten code (e.g. callee of
|
|
204
|
+
* `new ns.X(...)`); a type-only import creates no runtime binding. CJS
|
|
205
|
+
* `require` and TS `import = require` forms are always value bindings and
|
|
206
|
+
* remain included regardless.
|
|
207
|
+
*/
|
|
208
|
+
const getNamespaceLocalNames = (root, j, moduleName, opts = {}) => {
|
|
209
|
+
const localNames = new Set();
|
|
210
|
+
const { valueOnly = false } = opts;
|
|
211
|
+
const prefix = `${moduleName}/`;
|
|
212
|
+
const matchesModule = (source) => typeof source === "string" &&
|
|
213
|
+
(source === moduleName || source.startsWith(prefix));
|
|
214
|
+
// ESM: `import * as ns from "moduleName[/subpath]"`
|
|
215
|
+
root.find(j.ImportDeclaration).forEach((importPath) => {
|
|
216
|
+
if (!matchesModule(importPath.node.source.value))
|
|
217
|
+
return;
|
|
218
|
+
const declKind = importPath.node.importKind;
|
|
219
|
+
if (valueOnly && declKind === "type")
|
|
220
|
+
return;
|
|
221
|
+
for (const spec of importPath.node.specifiers ?? []) {
|
|
222
|
+
if (spec.type === "ImportNamespaceSpecifier" &&
|
|
223
|
+
spec.local?.type === "Identifier") {
|
|
224
|
+
localNames.add(spec.local.name);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
// TypeScript: `import ns = require("moduleName[/subpath]")`
|
|
229
|
+
root.find(j.TSImportEqualsDeclaration).forEach((importPath) => {
|
|
230
|
+
const ref = importPath.node.moduleReference;
|
|
231
|
+
if (ref.type !== "TSExternalModuleReference")
|
|
232
|
+
return;
|
|
233
|
+
if (!matchesModule((0, exports.getStringValue)(ref.expression)))
|
|
234
|
+
return;
|
|
235
|
+
if (importPath.node.id?.type === "Identifier") {
|
|
236
|
+
localNames.add(importPath.node.id.name);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
// CommonJS: `const ns = require("moduleName[/subpath]")`
|
|
240
|
+
// Top-level only — see the note in `getLocalNamesForImport` for why the
|
|
241
|
+
// collector and the call-site scope guard must agree on module scope.
|
|
242
|
+
root.find(j.CallExpression, {
|
|
243
|
+
callee: { type: "Identifier", name: "require" },
|
|
244
|
+
}).forEach((callPath) => {
|
|
245
|
+
const [arg] = callPath.node.arguments;
|
|
246
|
+
if (!arg || !matchesModule((0, exports.getStringValue)(arg)))
|
|
247
|
+
return;
|
|
248
|
+
if (callPath.scope?.isGlobal !== true)
|
|
249
|
+
return;
|
|
250
|
+
const parent = callPath.parent.node;
|
|
251
|
+
if (parent.type !== "VariableDeclarator" ||
|
|
252
|
+
parent.id.type !== "Identifier") {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const name = parent.id.name;
|
|
256
|
+
localNames.add(name);
|
|
257
|
+
});
|
|
258
|
+
return localNames;
|
|
259
|
+
};
|
|
260
|
+
exports.getNamespaceLocalNames = getNamespaceLocalNames;
|
|
135
261
|
/**
|
|
136
262
|
* Calls `callback` for each Identifier parameter found in function-like
|
|
137
263
|
* nodes (functions, methods, arrows) and TSParameterProperty nodes.
|
|
@@ -173,11 +299,16 @@ exports.TYPEORM_COLUMN_DECORATORS = new Set([
|
|
|
173
299
|
* the file — covers ESM aliases (`import { Column as C }`) and CJS aliases
|
|
174
300
|
* (`const { Column: C } = require(...)`). Returns a union set suitable for
|
|
175
301
|
* alias-aware identifier matching.
|
|
302
|
+
*
|
|
303
|
+
* @param opts.valueOnly Forwarded to `getLocalNamesForImport` — see there
|
|
304
|
+
* for semantics. Use when the expanded names will be matched against
|
|
305
|
+
* value-level usages (callees, method receivers) rather than type
|
|
306
|
+
* annotations.
|
|
176
307
|
*/
|
|
177
|
-
const expandLocalNamesForImports = (root, j, moduleName, importedNames) => {
|
|
308
|
+
const expandLocalNamesForImports = (root, j, moduleName, importedNames, opts = {}) => {
|
|
178
309
|
const expanded = new Set();
|
|
179
310
|
for (const name of importedNames) {
|
|
180
|
-
for (const local of (0, exports.getLocalNamesForImport)(root, j, moduleName, name)) {
|
|
311
|
+
for (const local of (0, exports.getLocalNamesForImport)(root, j, moduleName, name, opts)) {
|
|
181
312
|
expanded.add(local);
|
|
182
313
|
}
|
|
183
314
|
}
|
|
@@ -216,25 +347,258 @@ const forEachDecoratorObjectArg = (root, j, callback, decoratorNames) => {
|
|
|
216
347
|
});
|
|
217
348
|
};
|
|
218
349
|
exports.forEachDecoratorObjectArg = forEachDecoratorObjectArg;
|
|
350
|
+
/**
|
|
351
|
+
* Returns the key name for an `ObjectProperty` / `Property` node. Handles
|
|
352
|
+
* both identifier keys (`name`) and string-literal keys (`"name"`), including
|
|
353
|
+
* the computed-string form (`['name']: …`) whose runtime key is still
|
|
354
|
+
* statically knowable. Returns null for dynamic computed keys (`[someVar]`,
|
|
355
|
+
* `[expr]`), numeric keys, and for node types that don't carry a key at all
|
|
356
|
+
* (spread elements, etc.).
|
|
357
|
+
*/
|
|
358
|
+
const getObjectPropertyKeyName = (prop) => {
|
|
359
|
+
if (prop.type !== "Property" && prop.type !== "ObjectProperty")
|
|
360
|
+
return null;
|
|
361
|
+
// Computed keys whose expression is a string literal (`['foo']`) are
|
|
362
|
+
// equivalent at runtime to the plain-identifier form (`foo`), so match
|
|
363
|
+
// them. Dynamic computed keys (`[variable]`, `[fn()]`) stay opaque.
|
|
364
|
+
if (prop.computed)
|
|
365
|
+
return (0, exports.getStringValue)(prop.key);
|
|
366
|
+
if (prop.key.type === "Identifier")
|
|
367
|
+
return prop.key.name;
|
|
368
|
+
return (0, exports.getStringValue)(prop.key);
|
|
369
|
+
};
|
|
370
|
+
exports.getObjectPropertyKeyName = getObjectPropertyKeyName;
|
|
371
|
+
/**
|
|
372
|
+
* Walks `new ColumnMetadata({ args: { options: {...} } })` constructor calls
|
|
373
|
+
* and invokes `callback` with the inner `options` ObjectExpression. Mirrors
|
|
374
|
+
* `forEachDecoratorObjectArg` but for the ColumnMetadata class constructor:
|
|
375
|
+
* its `args.options` object is typed `ColumnOptions`, so transforms that
|
|
376
|
+
* rewrite `ColumnOptions` fields (`readonly` → `update`, `width`/`zerofill`
|
|
377
|
+
* removal) must also cover this path. Rare in user code but shows up in
|
|
378
|
+
* multi-tenant / metadata-manipulation patterns.
|
|
379
|
+
*
|
|
380
|
+
* Recognised import / binding shapes, all via
|
|
381
|
+
* `expandLocalNamesForImports` / `getNamespaceLocalNames`:
|
|
382
|
+
*
|
|
383
|
+
* import { ColumnMetadata } from "typeorm"
|
|
384
|
+
* import { ColumnMetadata as CM } from "typeorm"
|
|
385
|
+
* import { ColumnMetadata } from "typeorm/metadata/ColumnMetadata" // sub-path
|
|
386
|
+
* import * as typeorm from "typeorm" // namespace
|
|
387
|
+
* import typeorm = require("typeorm") // TS import-equals
|
|
388
|
+
* const { ColumnMetadata } = require("typeorm") // CJS destructure
|
|
389
|
+
* const typeorm = require("typeorm") // CJS namespace
|
|
390
|
+
*
|
|
391
|
+
* Type-only imports (declaration-level `import type { X }` and per-specifier
|
|
392
|
+
* `import { type X }`, same for `import type * as`) are filtered via
|
|
393
|
+
* `valueOnly: true` — `new X(...)` needs a runtime binding, and a type-only
|
|
394
|
+
* import creates none.
|
|
395
|
+
*
|
|
396
|
+
* Name-level matches are further gated by a scope check: the callee
|
|
397
|
+
* identifier must resolve to the module-level import binding and not a
|
|
398
|
+
* shadowing declaration in an inner scope (function parameter, nested
|
|
399
|
+
* `const`/`let`/`var`, class declaration, catch binding).
|
|
400
|
+
*/
|
|
401
|
+
const forEachColumnMetadataOptionsArg = (root, j, target, callback) => {
|
|
402
|
+
const classLocalNames = (0, exports.expandLocalNamesForImports)(root, j, target.moduleName, new Set([target.className]), { valueOnly: true });
|
|
403
|
+
const namespaceLocalNames = (0, exports.getNamespaceLocalNames)(root, j, target.moduleName, { valueOnly: true });
|
|
404
|
+
if (classLocalNames.size === 0 && namespaceLocalNames.size === 0)
|
|
405
|
+
return;
|
|
406
|
+
const resolvesToModuleScope = (path, name) => {
|
|
407
|
+
let scope = path.scope;
|
|
408
|
+
while (scope) {
|
|
409
|
+
if (scope.isGlobal === true || !scope.parent)
|
|
410
|
+
return true;
|
|
411
|
+
if (scope.declares(name))
|
|
412
|
+
return false;
|
|
413
|
+
scope = scope.parent;
|
|
414
|
+
}
|
|
415
|
+
return true;
|
|
416
|
+
};
|
|
417
|
+
const matchesCallee = (callee, path) => {
|
|
418
|
+
if (callee.type === "Identifier") {
|
|
419
|
+
if (!classLocalNames.has(callee.name))
|
|
420
|
+
return false;
|
|
421
|
+
return resolvesToModuleScope(path, callee.name);
|
|
422
|
+
}
|
|
423
|
+
if ((callee.type === "MemberExpression" ||
|
|
424
|
+
callee.type === "OptionalMemberExpression") &&
|
|
425
|
+
callee.computed !== true) {
|
|
426
|
+
const obj = callee.object;
|
|
427
|
+
const prop = callee.property;
|
|
428
|
+
if ((0, exports.isIdentifier)(obj) &&
|
|
429
|
+
(0, exports.isIdentifier)(prop) &&
|
|
430
|
+
prop.name === target.className) {
|
|
431
|
+
if (!namespaceLocalNames.has(obj.name))
|
|
432
|
+
return false;
|
|
433
|
+
return resolvesToModuleScope(path, obj.name);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return false;
|
|
437
|
+
};
|
|
438
|
+
// Returns the property value as an ObjectExpression if the property with
|
|
439
|
+
// the given key exists and its value is an ObjectExpression (after peeling
|
|
440
|
+
// TS wrappers like `as` / `satisfies` / `!` / parens). Uses `findLast` so
|
|
441
|
+
// duplicate keys match JS object-literal semantics (last occurrence wins).
|
|
442
|
+
const findObjectPropertyValue = (obj, keyName) => {
|
|
443
|
+
const prop = obj.properties.findLast((p) => (0, exports.getObjectPropertyKeyName)(p) === keyName);
|
|
444
|
+
if (!prop ||
|
|
445
|
+
(prop.type !== "Property" && prop.type !== "ObjectProperty")) {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
const value = (0, exports.unwrapTsExpression)(prop.value);
|
|
449
|
+
return value.type === "ObjectExpression" ? value : null;
|
|
450
|
+
};
|
|
451
|
+
root.find(j.NewExpression).forEach((path) => {
|
|
452
|
+
if (!matchesCallee(path.node.callee, path))
|
|
453
|
+
return;
|
|
454
|
+
const [firstArg] = path.node.arguments;
|
|
455
|
+
const arg = firstArg ? (0, exports.unwrapTsExpression)(firstArg) : undefined;
|
|
456
|
+
if (arg?.type !== "ObjectExpression")
|
|
457
|
+
return;
|
|
458
|
+
const argsValue = findObjectPropertyValue(arg, "args");
|
|
459
|
+
if (!argsValue)
|
|
460
|
+
return;
|
|
461
|
+
const optionsValue = findObjectPropertyValue(argsValue, "options");
|
|
462
|
+
if (!optionsValue)
|
|
463
|
+
return;
|
|
464
|
+
callback(optionsValue, path);
|
|
465
|
+
});
|
|
466
|
+
};
|
|
467
|
+
exports.forEachColumnMetadataOptionsArg = forEachColumnMetadataOptionsArg;
|
|
468
|
+
/**
|
|
469
|
+
* TypeORM `Repository` / `EntityManager` methods that accept a FindOptions
|
|
470
|
+
* object (with `select`/`relations`/`where`/…). Used to scope the
|
|
471
|
+
* `select: [...]` / `relations: [...]` → object-form transforms to
|
|
472
|
+
* arguments passed into these methods. Deliberately excludes the
|
|
473
|
+
* `*By` variants (`findBy`, `findOneBy`, `findOneByOrFail`,
|
|
474
|
+
* `findAndCountBy`, `countBy`, `existsBy`) — those accept a plain WHERE
|
|
475
|
+
* object, so rewriting a top-level `select` or `relations` key there would
|
|
476
|
+
* mangle matches against entity fields of those names.
|
|
477
|
+
*
|
|
478
|
+
* The method-name check (rather than a file-level typeorm import gate)
|
|
479
|
+
* lets the transforms fire in NestJS-style service files that only pull
|
|
480
|
+
* TypeORM types via a wrapper module.
|
|
481
|
+
*/
|
|
482
|
+
exports.TYPEORM_FIND_OPTIONS_METHODS = new Set([
|
|
483
|
+
"find",
|
|
484
|
+
"findAndCount",
|
|
485
|
+
"findOne",
|
|
486
|
+
"findOneOrFail",
|
|
487
|
+
"count",
|
|
488
|
+
"exists",
|
|
489
|
+
]);
|
|
490
|
+
/**
|
|
491
|
+
* Returns true when `node` is a call of shape `Object.fromEntries(...)`.
|
|
492
|
+
* Used by the find-option transforms to stay idempotent — a second pass
|
|
493
|
+
* must not wrap an already-wrapped dynamic value in another `fromEntries`.
|
|
494
|
+
*/
|
|
495
|
+
const isObjectFromEntriesCall = (node) => {
|
|
496
|
+
if (node.type !== "CallExpression")
|
|
497
|
+
return false;
|
|
498
|
+
const callee = node.callee;
|
|
499
|
+
if (callee.type !== "MemberExpression")
|
|
500
|
+
return false;
|
|
501
|
+
const m = callee;
|
|
502
|
+
return (m.object.type === "Identifier" &&
|
|
503
|
+
m.object.name === "Object" &&
|
|
504
|
+
m.property.type === "Identifier" &&
|
|
505
|
+
m.property.name === "fromEntries");
|
|
506
|
+
};
|
|
507
|
+
exports.isObjectFromEntriesCall = isObjectFromEntriesCall;
|
|
508
|
+
/**
|
|
509
|
+
* Returns true when the given `ObjectProperty` lives inside an object that
|
|
510
|
+
* is an argument to one of the TYPEORM_FIND_OPTIONS_METHODS. Matches both
|
|
511
|
+
* `repo.find({ select: [...] })` (object is the single argument) and
|
|
512
|
+
* `manager.find(Entity, { select: [...] })` (object is the second arg).
|
|
513
|
+
*/
|
|
514
|
+
const isFindMethodCallArgument = (propPath) => {
|
|
515
|
+
const objExprPath = propPath.parent;
|
|
516
|
+
if (objExprPath?.node.type !== "ObjectExpression") {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
// Walk up through TS expression wrappers (`{ select: [...] } as T`,
|
|
520
|
+
// `satisfies Opts`, parens) before expecting a CallExpression. Without
|
|
521
|
+
// this, `repo.find({ select: [...] } as FindOptions)` is missed.
|
|
522
|
+
let ancestor = objExprPath.parent;
|
|
523
|
+
while (ancestor) {
|
|
524
|
+
const t = ancestor.node.type;
|
|
525
|
+
if (t === "TSAsExpression" ||
|
|
526
|
+
t === "TSSatisfiesExpression" ||
|
|
527
|
+
t === "TSTypeAssertion" ||
|
|
528
|
+
t === "TSNonNullExpression" ||
|
|
529
|
+
t === "ParenthesizedExpression") {
|
|
530
|
+
ancestor = ancestor.parent;
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
if (!ancestor)
|
|
536
|
+
return false;
|
|
537
|
+
const callNode = ancestor.node;
|
|
538
|
+
if (callNode.type !== "CallExpression" &&
|
|
539
|
+
callNode.type !== "OptionalCallExpression") {
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
const callee = callNode.callee;
|
|
543
|
+
if (callee.type !== "MemberExpression" &&
|
|
544
|
+
callee.type !== "OptionalMemberExpression") {
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
const prop = callee.property;
|
|
548
|
+
if (prop.type !== "Identifier")
|
|
549
|
+
return false;
|
|
550
|
+
return exports.TYPEORM_FIND_OPTIONS_METHODS.has(prop.name);
|
|
551
|
+
};
|
|
552
|
+
exports.isFindMethodCallArgument = isFindMethodCallArgument;
|
|
553
|
+
/**
|
|
554
|
+
* Peels TypeScript expression wrappers and parenthesized expressions around
|
|
555
|
+
* a value so callers see the underlying node. Handles `as X` / `x!` /
|
|
556
|
+
* `x satisfies X` / `<X>x` / `(x)`. Used by transforms that inspect values
|
|
557
|
+
* which users may annotate with type assertions — e.g.
|
|
558
|
+
* `type: "expo" as const`, `type: ("mongodb")`,
|
|
559
|
+
* `{ logPath: "x" } as Options`.
|
|
560
|
+
*
|
|
561
|
+
* Generic over the node type so callers can keep their original narrowing
|
|
562
|
+
* — in practice the returned node is the same type as the input (with TS
|
|
563
|
+
* wrapper variants peeled off).
|
|
564
|
+
*/
|
|
565
|
+
const unwrapTsExpression = (node) => {
|
|
566
|
+
let current = node;
|
|
567
|
+
while (current.type === "TSAsExpression" ||
|
|
568
|
+
current.type === "TSNonNullExpression" ||
|
|
569
|
+
current.type === "TSSatisfiesExpression" ||
|
|
570
|
+
current.type === "TSTypeAssertion" ||
|
|
571
|
+
current.type === "ParenthesizedExpression") {
|
|
572
|
+
const inner = current.expression;
|
|
573
|
+
if (!inner)
|
|
574
|
+
break;
|
|
575
|
+
current = inner;
|
|
576
|
+
}
|
|
577
|
+
return current;
|
|
578
|
+
};
|
|
579
|
+
exports.unwrapTsExpression = unwrapTsExpression;
|
|
219
580
|
/**
|
|
220
581
|
* Removes properties matching the given key names from an ObjectExpression.
|
|
221
582
|
* Matches both identifier keys (`name`) and string-literal keys (`"name"`).
|
|
222
583
|
* Returns true if any properties were removed.
|
|
223
584
|
*/
|
|
224
|
-
const removeObjectProperties = (obj, propertyNames) => {
|
|
585
|
+
const removeObjectProperties = (obj, propertyNames) => (0, exports.removeObjectPropertiesWhere)(obj, (prop) => {
|
|
586
|
+
const key = (0, exports.getObjectPropertyKeyName)(prop);
|
|
587
|
+
return key !== null && propertyNames.has(key);
|
|
588
|
+
});
|
|
589
|
+
exports.removeObjectProperties = removeObjectProperties;
|
|
590
|
+
/**
|
|
591
|
+
* Removes every property from `obj` that satisfies `predicate`. Returns true
|
|
592
|
+
* if any property was removed. Use this over `removeObjectProperties` when
|
|
593
|
+
* removal needs to inspect the property value (not just the key name) —
|
|
594
|
+
* e.g. "remove `driver` only when its value is `require("expo-sqlite")`".
|
|
595
|
+
*/
|
|
596
|
+
const removeObjectPropertiesWhere = (obj, predicate) => {
|
|
225
597
|
const original = obj.properties.length;
|
|
226
|
-
obj.properties = obj.properties.filter((prop) =>
|
|
227
|
-
if (prop.type !== "Property" && prop.type !== "ObjectProperty") {
|
|
228
|
-
return true;
|
|
229
|
-
}
|
|
230
|
-
const keyName = prop.key.type === "Identifier"
|
|
231
|
-
? prop.key.name
|
|
232
|
-
: (0, exports.getStringValue)(prop.key);
|
|
233
|
-
return keyName === null ? true : !propertyNames.has(keyName);
|
|
234
|
-
});
|
|
598
|
+
obj.properties = obj.properties.filter((prop) => !predicate(prop));
|
|
235
599
|
return obj.properties.length !== original;
|
|
236
600
|
};
|
|
237
|
-
exports.
|
|
601
|
+
exports.removeObjectPropertiesWhere = removeObjectPropertiesWhere;
|
|
238
602
|
/**
|
|
239
603
|
* Finds imports from a module, removes the specified named import specifiers,
|
|
240
604
|
* and removes the entire import declaration if no specifiers remain.
|
|
@@ -264,6 +628,77 @@ const removeImportSpecifiers = (root, j, moduleName, specifierNames) => {
|
|
|
264
628
|
return removed;
|
|
265
629
|
};
|
|
266
630
|
exports.removeImportSpecifiers = removeImportSpecifiers;
|
|
631
|
+
// Returns true for `"moduleName"` and any sub-path like `"moduleName/..."`.
|
|
632
|
+
// Used so re-export helpers can match `export { X } from "typeorm"` as well
|
|
633
|
+
// as `export { SapConnectionOptions } from "typeorm/driver/sap/SapConnectionOptions"`.
|
|
634
|
+
const matchesModuleOrSubPath = (source, moduleName) => typeof source === "string" &&
|
|
635
|
+
(source === moduleName || source.startsWith(`${moduleName}/`));
|
|
636
|
+
/**
|
|
637
|
+
* Finds re-exports from a module (`export { X } from "module"`) and removes
|
|
638
|
+
* the named specifiers listed in `specifierNames`. Also matches sub-path
|
|
639
|
+
* re-exports (`export { X } from "module/sub/path"`). Removes the entire
|
|
640
|
+
* `ExportNamedDeclaration` if no specifiers remain. Returns true if any
|
|
641
|
+
* specifiers were removed.
|
|
642
|
+
*/
|
|
643
|
+
const removeReExportSpecifiers = (root, j, moduleName, specifierNames) => {
|
|
644
|
+
let removed = false;
|
|
645
|
+
root.find(j.ExportNamedDeclaration).forEach((exportPath) => {
|
|
646
|
+
const source = exportPath.node.source?.value;
|
|
647
|
+
if (!matchesModuleOrSubPath(source, moduleName))
|
|
648
|
+
return;
|
|
649
|
+
const remaining = exportPath.node.specifiers?.filter((spec) => {
|
|
650
|
+
if (spec.type === "ExportSpecifier" &&
|
|
651
|
+
spec.local?.type === "Identifier" &&
|
|
652
|
+
specifierNames.has(spec.local.name)) {
|
|
653
|
+
removed = true;
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
return true;
|
|
657
|
+
});
|
|
658
|
+
if (remaining?.length === 0) {
|
|
659
|
+
j(exportPath).remove();
|
|
660
|
+
}
|
|
661
|
+
else if (remaining) {
|
|
662
|
+
exportPath.node.specifiers = remaining;
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
return removed;
|
|
666
|
+
};
|
|
667
|
+
exports.removeReExportSpecifiers = removeReExportSpecifiers;
|
|
668
|
+
/**
|
|
669
|
+
* Finds re-exports from a module (`export { X } from "module"`) and renames
|
|
670
|
+
* specifiers according to the `renames` map. Also matches sub-path
|
|
671
|
+
* re-exports (`export { X } from "module/sub/path"`). When the re-export has
|
|
672
|
+
* an alias (`export { X as Y }`), only the local name is renamed so
|
|
673
|
+
* downstream consumers continue to see the same exported name. Returns true
|
|
674
|
+
* if any specifiers were renamed.
|
|
675
|
+
*/
|
|
676
|
+
const renameReExportSpecifiers = (root, j, moduleName, renames) => {
|
|
677
|
+
let renamed = false;
|
|
678
|
+
root.find(j.ExportNamedDeclaration).forEach((exportPath) => {
|
|
679
|
+
const source = exportPath.node.source?.value;
|
|
680
|
+
if (!matchesModuleOrSubPath(source, moduleName))
|
|
681
|
+
return;
|
|
682
|
+
exportPath.node.specifiers?.forEach((spec) => {
|
|
683
|
+
if (spec.type !== "ExportSpecifier" ||
|
|
684
|
+
spec.local?.type !== "Identifier") {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const newName = renames[spec.local.name];
|
|
688
|
+
if (!newName)
|
|
689
|
+
return;
|
|
690
|
+
const wasAlias = spec.exported.type === "Identifier" &&
|
|
691
|
+
spec.exported.name !== spec.local.name;
|
|
692
|
+
spec.local.name = newName;
|
|
693
|
+
if (!wasAlias && spec.exported.type === "Identifier") {
|
|
694
|
+
spec.exported.name = newName;
|
|
695
|
+
}
|
|
696
|
+
renamed = true;
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
return renamed;
|
|
700
|
+
};
|
|
701
|
+
exports.renameReExportSpecifiers = renameReExportSpecifiers;
|
|
267
702
|
/**
|
|
268
703
|
* Finds CallExpression nodes with a MemberExpression callee where the
|
|
269
704
|
* property matches `oldName`, and renames the property to `newName`.
|
|
@@ -286,4 +721,363 @@ const renameMemberMethod = (root, j, oldName, newName) => {
|
|
|
286
721
|
return renamed;
|
|
287
722
|
};
|
|
288
723
|
exports.renameMemberMethod = renameMemberMethod;
|
|
724
|
+
/**
|
|
725
|
+
* Walks a TypeScript type-annotation node and returns the root identifier
|
|
726
|
+
* name — e.g. `Repository<User>` → `"Repository"`, `Repository<User> | null`
|
|
727
|
+
* → `"Repository"`, `typeof Repository` → `"Repository"`. Returns null when
|
|
728
|
+
* the annotation doesn't root on a TSTypeReference with an Identifier name.
|
|
729
|
+
*
|
|
730
|
+
* When `typeormNamespaceNames` is passed, qualified references like
|
|
731
|
+
* `ns.Repository<User>` are only returned if `ns` is one of the provided
|
|
732
|
+
* namespace locals — otherwise the qualified reference is rejected so that
|
|
733
|
+
* unrelated namespaces (`Foo.Repository<T>` from some-other-lib) are not
|
|
734
|
+
* mistaken for TypeORM types. Without the parameter, any qualifier is
|
|
735
|
+
* accepted, which matches legacy callers that don't care about provenance.
|
|
736
|
+
*/
|
|
737
|
+
// Walks the members of a union/intersection type and prefers the first
|
|
738
|
+
// TypeORM-family name (so `FooBar | Repository<T>` or `null | Repository<T>`
|
|
739
|
+
// classify correctly). Falls back to the first non-null root name to keep
|
|
740
|
+
// pre-existing behavior for callers that don't care about TypeORM gating.
|
|
741
|
+
const findUnionOrIntersectionRoot = (types, typeormNamespaceNames) => {
|
|
742
|
+
let firstName = null;
|
|
743
|
+
for (const member of types) {
|
|
744
|
+
const name = (0, exports.getTypeReferenceRootName)(member, typeormNamespaceNames);
|
|
745
|
+
if (!name)
|
|
746
|
+
continue;
|
|
747
|
+
if (exports.TYPEORM_REPOSITORY_TYPES.has(name) ||
|
|
748
|
+
exports.TYPEORM_DATASOURCE_TYPES.has(name)) {
|
|
749
|
+
return name;
|
|
750
|
+
}
|
|
751
|
+
firstName ??= name;
|
|
752
|
+
}
|
|
753
|
+
return firstName;
|
|
754
|
+
};
|
|
755
|
+
// Returns the rightmost segment of a `TSQualifiedName` (`a.b.c` in type
|
|
756
|
+
// position) — always an `Identifier` in valid TS, since the nesting lives
|
|
757
|
+
// on `.left`. When `typeormNamespaceNames` is provided, the leftmost segment
|
|
758
|
+
// is checked against the set of typeorm namespace locals and the rightmost
|
|
759
|
+
// name is only returned if the qualifier matches — so `typeorm.Repository`
|
|
760
|
+
// resolves to `"Repository"` while `other.Repository` returns null, avoiding
|
|
761
|
+
// false-positive classification of identically-named types from other
|
|
762
|
+
// modules. Without the parameter, any qualifier is accepted (legacy
|
|
763
|
+
// behaviour for callers that only look at the rightmost segment).
|
|
764
|
+
const getQualifiedNameRightmost = (node, typeormNamespaceNames) => {
|
|
765
|
+
const n = node;
|
|
766
|
+
if (n.right.type !== "Identifier")
|
|
767
|
+
return null;
|
|
768
|
+
if (typeormNamespaceNames !== undefined) {
|
|
769
|
+
// Walk down `.left` to the leftmost identifier (qualifier root).
|
|
770
|
+
let leftmost = n.left;
|
|
771
|
+
while (leftmost.type === "TSQualifiedName") {
|
|
772
|
+
leftmost = leftmost.left;
|
|
773
|
+
}
|
|
774
|
+
if (leftmost.type !== "Identifier")
|
|
775
|
+
return null;
|
|
776
|
+
if (!typeormNamespaceNames.has(leftmost.name))
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
return n.right.name;
|
|
780
|
+
};
|
|
781
|
+
const getTypeReferenceRootName = (node, typeormNamespaceNames) => {
|
|
782
|
+
if (!node)
|
|
783
|
+
return null;
|
|
784
|
+
if (node.type === "TSTypeReference") {
|
|
785
|
+
const n = node;
|
|
786
|
+
if (n.typeName.type === "Identifier") {
|
|
787
|
+
return n.typeName.name;
|
|
788
|
+
}
|
|
789
|
+
// `typeorm.Repository<User>` — TSTypeReference wraps a TSQualifiedName
|
|
790
|
+
// (namespace-import access). Without this branch,
|
|
791
|
+
// `import * as typeorm from "typeorm"` callers would be missed.
|
|
792
|
+
if (n.typeName.type === "TSQualifiedName") {
|
|
793
|
+
return getQualifiedNameRightmost(n.typeName, typeormNamespaceNames);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
// `const X: typeof Repository` — TSTypeQuery wraps the referenced
|
|
797
|
+
// identifier in `exprName`. Without this branch, type-of annotations on
|
|
798
|
+
// typeorm-family types wouldn't register as repository bindings.
|
|
799
|
+
if (node.type === "TSTypeQuery") {
|
|
800
|
+
const n = node;
|
|
801
|
+
if (n.exprName.type === "Identifier") {
|
|
802
|
+
return n.exprName.name;
|
|
803
|
+
}
|
|
804
|
+
if (n.exprName.type === "TSQualifiedName") {
|
|
805
|
+
return getQualifiedNameRightmost(n.exprName, typeormNamespaceNames);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if (node.type === "TSTypeAnnotation") {
|
|
809
|
+
const n = node;
|
|
810
|
+
return (0, exports.getTypeReferenceRootName)(n.typeAnnotation, typeormNamespaceNames);
|
|
811
|
+
}
|
|
812
|
+
if (node.type === "TSUnionType" || node.type === "TSIntersectionType") {
|
|
813
|
+
return findUnionOrIntersectionRoot(node.types, typeormNamespaceNames);
|
|
814
|
+
}
|
|
815
|
+
return null;
|
|
816
|
+
};
|
|
817
|
+
exports.getTypeReferenceRootName = getTypeReferenceRootName;
|
|
818
|
+
/**
|
|
819
|
+
* TypeORM Repository-family type names used to detect bindings that hold a
|
|
820
|
+
* Repository/EntityManager instance, so transforms can scope method renames
|
|
821
|
+
* (`.findByIds`, `.findOneById`, `.exist`, `.stats`) to those receivers.
|
|
822
|
+
*/
|
|
823
|
+
exports.TYPEORM_REPOSITORY_TYPES = new Set([
|
|
824
|
+
"Repository",
|
|
825
|
+
"TreeRepository",
|
|
826
|
+
"MongoRepository",
|
|
827
|
+
"EntityRepository",
|
|
828
|
+
"AbstractRepository",
|
|
829
|
+
"EntityManager",
|
|
830
|
+
"MongoEntityManager",
|
|
831
|
+
"SqljsEntityManager",
|
|
832
|
+
]);
|
|
833
|
+
/**
|
|
834
|
+
* Call shapes whose return value is a Repository/EntityManager:
|
|
835
|
+
* `.getRepository(...)`, `.getMongoRepository(...)`, `.getTreeRepository(...)`,
|
|
836
|
+
* `.getCustomRepository(...)`, `.manager` (property access, see caller).
|
|
837
|
+
*/
|
|
838
|
+
const REPOSITORY_RETURNING_METHODS = new Set([
|
|
839
|
+
"getRepository",
|
|
840
|
+
"getMongoRepository",
|
|
841
|
+
"getTreeRepository",
|
|
842
|
+
"getCustomRepository",
|
|
843
|
+
]);
|
|
844
|
+
// Returns true when `node` is a call expression whose callee ends in one of
|
|
845
|
+
// the Repository-returning method names (e.g. `ds.getRepository(User)`).
|
|
846
|
+
const isRepositoryReturningCall = (node) => {
|
|
847
|
+
if (node.type !== "CallExpression" &&
|
|
848
|
+
node.type !== "OptionalCallExpression")
|
|
849
|
+
return false;
|
|
850
|
+
const call = node;
|
|
851
|
+
const callee = call.callee;
|
|
852
|
+
if (callee.type !== "MemberExpression" &&
|
|
853
|
+
callee.type !== "OptionalMemberExpression")
|
|
854
|
+
return false;
|
|
855
|
+
const member = callee;
|
|
856
|
+
if (member.property.type === "Identifier") {
|
|
857
|
+
return REPOSITORY_RETURNING_METHODS.has(member.property.name);
|
|
858
|
+
}
|
|
859
|
+
if (member.computed) {
|
|
860
|
+
const name = (0, exports.getStringValue)(member.property);
|
|
861
|
+
return name !== null && REPOSITORY_RETURNING_METHODS.has(name);
|
|
862
|
+
}
|
|
863
|
+
return false;
|
|
864
|
+
};
|
|
865
|
+
/**
|
|
866
|
+
* TypeORM DataSource-family type names, recognized on local bindings so
|
|
867
|
+
* `dataSource.manager.X()` can be classified as a Repository receiver via
|
|
868
|
+
* the `.manager` accessor chain.
|
|
869
|
+
*/
|
|
870
|
+
exports.TYPEORM_DATASOURCE_TYPES = new Set([
|
|
871
|
+
"DataSource",
|
|
872
|
+
"Connection",
|
|
873
|
+
]);
|
|
874
|
+
// Classifies a type name into one of the tracked receiver buckets, or
|
|
875
|
+
// returns null when the type isn't a TypeORM receiver we care about.
|
|
876
|
+
const classifyRepositoryTypeName = (typeName, bindings, forThisMember) => {
|
|
877
|
+
if (exports.TYPEORM_REPOSITORY_TYPES.has(typeName)) {
|
|
878
|
+
return forThisMember ? bindings.classProps : bindings.locals;
|
|
879
|
+
}
|
|
880
|
+
if (exports.TYPEORM_DATASOURCE_TYPES.has(typeName)) {
|
|
881
|
+
return forThisMember
|
|
882
|
+
? bindings.dataSourceClassProps
|
|
883
|
+
: bindings.dataSourceLocals;
|
|
884
|
+
}
|
|
885
|
+
return null;
|
|
886
|
+
};
|
|
887
|
+
// Records a typed identifier (variable / parameter) into the appropriate
|
|
888
|
+
// binding bucket based on its TypeScript annotation.
|
|
889
|
+
const recordTypedIdentifier = (id, annotation, bindings, typeormNamespaceNames) => {
|
|
890
|
+
if (id.type !== "Identifier")
|
|
891
|
+
return;
|
|
892
|
+
const typeName = (0, exports.getTypeReferenceRootName)(annotation, typeormNamespaceNames);
|
|
893
|
+
if (!typeName)
|
|
894
|
+
return;
|
|
895
|
+
const bucket = classifyRepositoryTypeName(typeName, bindings, false);
|
|
896
|
+
bucket?.add(id.name);
|
|
897
|
+
};
|
|
898
|
+
// Resolves the underlying `Identifier` inside a function parameter — peeling
|
|
899
|
+
// `AssignmentPattern` (default params) and `TSParameterProperty` (constructor
|
|
900
|
+
// parameter properties). `isParameterProperty` is true only for
|
|
901
|
+
// TSParameterProperty, so callers can additionally record it as a
|
|
902
|
+
// `this.<name>` class member.
|
|
903
|
+
const unwrapParameterIdentifier = (param) => {
|
|
904
|
+
if (param.type === "Identifier") {
|
|
905
|
+
return {
|
|
906
|
+
id: param,
|
|
907
|
+
annotation: param.typeAnnotation ?? null,
|
|
908
|
+
isParameterProperty: false,
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
if (param.type !== "AssignmentPattern" &&
|
|
912
|
+
param.type !== "TSParameterProperty") {
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
const inner = param.left ??
|
|
916
|
+
param.parameter;
|
|
917
|
+
if (inner?.type !== "Identifier")
|
|
918
|
+
return null;
|
|
919
|
+
return {
|
|
920
|
+
id: inner,
|
|
921
|
+
annotation: inner.typeAnnotation ?? null,
|
|
922
|
+
isParameterProperty: param.type === "TSParameterProperty",
|
|
923
|
+
};
|
|
924
|
+
};
|
|
925
|
+
// Scans VariableDeclarators for typed identifiers and initializers that
|
|
926
|
+
// return a Repository via `.getRepository()`-family calls.
|
|
927
|
+
const collectDeclaratorBindings = (root, j, bindings, typeormNamespaceNames) => {
|
|
928
|
+
root.find(j.VariableDeclarator).forEach((p) => {
|
|
929
|
+
const id = p.node.id;
|
|
930
|
+
if (id.type !== "Identifier")
|
|
931
|
+
return;
|
|
932
|
+
recordTypedIdentifier(id, id.typeAnnotation ?? null, bindings, typeormNamespaceNames);
|
|
933
|
+
const init = p.node.init;
|
|
934
|
+
if (init && isRepositoryReturningCall(init)) {
|
|
935
|
+
bindings.locals.add(id.name);
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
root.find(j.AssignmentExpression).forEach((p) => {
|
|
939
|
+
const left = p.node.left;
|
|
940
|
+
if (left.type !== "Identifier")
|
|
941
|
+
return;
|
|
942
|
+
if (!isRepositoryReturningCall(p.node.right))
|
|
943
|
+
return;
|
|
944
|
+
bindings.locals.add(left.name);
|
|
945
|
+
});
|
|
946
|
+
};
|
|
947
|
+
// Scans function parameters on every function-like node. TSParameterProperty
|
|
948
|
+
// bindings (`constructor(private repo: Repository<T>)`) are tracked in both
|
|
949
|
+
// `locals` (for accesses inside the constructor body) AND `classProps` (for
|
|
950
|
+
// `this.repo` accesses elsewhere in the class).
|
|
951
|
+
const collectFunctionParamBindings = (root, j, bindings, typeormNamespaceNames) => {
|
|
952
|
+
const visit = (node) => {
|
|
953
|
+
for (const param of node.params) {
|
|
954
|
+
const unwrapped = unwrapParameterIdentifier(param);
|
|
955
|
+
if (!unwrapped)
|
|
956
|
+
continue;
|
|
957
|
+
recordTypedIdentifier(unwrapped.id, unwrapped.annotation, bindings, typeormNamespaceNames);
|
|
958
|
+
if (unwrapped.isParameterProperty &&
|
|
959
|
+
unwrapped.id.type === "Identifier") {
|
|
960
|
+
const typeName = (0, exports.getTypeReferenceRootName)(unwrapped.annotation, typeormNamespaceNames);
|
|
961
|
+
if (typeName) {
|
|
962
|
+
const classBucket = classifyRepositoryTypeName(typeName, bindings, true);
|
|
963
|
+
classBucket?.add(unwrapped.id.name);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
root.find(j.FunctionDeclaration).forEach((p) => visit(p.node));
|
|
969
|
+
root.find(j.FunctionExpression).forEach((p) => visit(p.node));
|
|
970
|
+
root.find(j.ArrowFunctionExpression).forEach((p) => visit(p.node));
|
|
971
|
+
root.find(j.ClassMethod).forEach((p) => visit(p.node));
|
|
972
|
+
root.find(j.TSDeclareMethod).forEach((p) => visit(p.node));
|
|
973
|
+
};
|
|
974
|
+
// Scans class properties — `this.<name>` receivers are tracked separately
|
|
975
|
+
// from local identifiers because the access shape is different.
|
|
976
|
+
const collectClassPropertyBindings = (root, j, bindings, typeormNamespaceNames) => {
|
|
977
|
+
root.find(j.ClassProperty).forEach((p) => {
|
|
978
|
+
const key = p.node.key;
|
|
979
|
+
if (key.type !== "Identifier")
|
|
980
|
+
return;
|
|
981
|
+
const annotation = p.node.typeAnnotation ?? null;
|
|
982
|
+
const typeName = (0, exports.getTypeReferenceRootName)(annotation, typeormNamespaceNames);
|
|
983
|
+
if (!typeName)
|
|
984
|
+
return;
|
|
985
|
+
const bucket = classifyRepositoryTypeName(typeName, bindings, true);
|
|
986
|
+
bucket?.add(key.name);
|
|
987
|
+
});
|
|
988
|
+
};
|
|
989
|
+
const collectRepositoryBindings = (root, j) => {
|
|
990
|
+
const bindings = {
|
|
991
|
+
locals: new Set(),
|
|
992
|
+
classProps: new Set(),
|
|
993
|
+
dataSourceLocals: new Set(),
|
|
994
|
+
dataSourceClassProps: new Set(),
|
|
995
|
+
};
|
|
996
|
+
// Qualified type references like `typeorm.Repository<T>` must only be
|
|
997
|
+
// classified as TypeORM receivers when the qualifier is an actual
|
|
998
|
+
// namespace-local of `import * as typeorm from "typeorm"` (or the
|
|
999
|
+
// `import ns = require("typeorm")` variants). Without this gate, a
|
|
1000
|
+
// file containing both a typeorm import and `Foo.Repository<T>` from
|
|
1001
|
+
// some-other-lib would treat `Foo.Repository`-typed bindings as
|
|
1002
|
+
// Repository receivers and mis-rewrite their method calls.
|
|
1003
|
+
const typeormNamespaceNames = (0, exports.getNamespaceLocalNames)(root, j, "typeorm");
|
|
1004
|
+
collectDeclaratorBindings(root, j, bindings, typeormNamespaceNames);
|
|
1005
|
+
collectFunctionParamBindings(root, j, bindings, typeormNamespaceNames);
|
|
1006
|
+
collectClassPropertyBindings(root, j, bindings, typeormNamespaceNames);
|
|
1007
|
+
return bindings;
|
|
1008
|
+
};
|
|
1009
|
+
exports.collectRepositoryBindings = collectRepositoryBindings;
|
|
1010
|
+
/**
|
|
1011
|
+
* Returns true when `receiver` is a MemberExpression-eligible node whose
|
|
1012
|
+
* root identifier is a Repository-bound local or `this.X` where `X` is a
|
|
1013
|
+
* Repository-typed class property. Also accepts fresh inline
|
|
1014
|
+
* `.getRepository(...)` call-expression receivers and DataSource-typed
|
|
1015
|
+
* bindings dereferenced via `.manager` (e.g. `ds.manager.findByIds(...)`).
|
|
1016
|
+
*
|
|
1017
|
+
* When the file contains no Repository-typed bindings at all (no typed
|
|
1018
|
+
* variables, no `.getRepository()` assignments, no Repository class
|
|
1019
|
+
* properties), falls back to permissive matching — the codemod has no
|
|
1020
|
+
* type signal and must trust the historical `fileImportsFrom("typeorm")`
|
|
1021
|
+
* file-level guard. Callers should add negative fixtures to pin the
|
|
1022
|
+
* common false-positive vectors.
|
|
1023
|
+
*/
|
|
1024
|
+
// Matches `this.<prop>` where `<prop>` is a tracked DataSource class prop.
|
|
1025
|
+
const isThisDataSourceProp = (node, dsClassProps) => (node.type === "MemberExpression" ||
|
|
1026
|
+
node.type === "OptionalMemberExpression") &&
|
|
1027
|
+
node.object.type === "ThisExpression" &&
|
|
1028
|
+
node.property.type === "Identifier" &&
|
|
1029
|
+
(dsClassProps?.has(node.property.name) ??
|
|
1030
|
+
false);
|
|
1031
|
+
// `<ds>.manager` — DataSource local/class-prop's `.manager` accessor
|
|
1032
|
+
// returns an EntityManager, whose find-family methods match Repository's.
|
|
1033
|
+
const isDataSourceManagerChain = (member, dsLocals, dsClassProps) => {
|
|
1034
|
+
if (member.property.type !== "Identifier" ||
|
|
1035
|
+
member.property.name !== "manager") {
|
|
1036
|
+
return false;
|
|
1037
|
+
}
|
|
1038
|
+
if (member.object.type === "Identifier" &&
|
|
1039
|
+
(dsLocals?.has(member.object.name) ?? false)) {
|
|
1040
|
+
return true;
|
|
1041
|
+
}
|
|
1042
|
+
return isThisDataSourceProp(member.object, dsClassProps);
|
|
1043
|
+
};
|
|
1044
|
+
// Classifies MemberExpression / OptionalMemberExpression receivers.
|
|
1045
|
+
const classifyMemberReceiver = (receiver, bindings, noBindingsFound) => {
|
|
1046
|
+
const member = receiver;
|
|
1047
|
+
if (isDataSourceManagerChain(member, bindings.dataSourceLocals, bindings.dataSourceClassProps)) {
|
|
1048
|
+
return true;
|
|
1049
|
+
}
|
|
1050
|
+
if (member.object.type === "ThisExpression" &&
|
|
1051
|
+
member.property.type === "Identifier") {
|
|
1052
|
+
if (bindings.classProps.has(member.property.name))
|
|
1053
|
+
return true;
|
|
1054
|
+
return noBindingsFound;
|
|
1055
|
+
}
|
|
1056
|
+
// Chained access like `service.userRepo.findByIds(...)` — accept only
|
|
1057
|
+
// when we have no binding info to disambiguate.
|
|
1058
|
+
return noBindingsFound;
|
|
1059
|
+
};
|
|
1060
|
+
const isRepositoryReceiver = (receiver, bindings) => {
|
|
1061
|
+
const noBindingsFound = bindings.locals.size === 0 &&
|
|
1062
|
+
bindings.classProps.size === 0 &&
|
|
1063
|
+
(bindings.dataSourceLocals?.size ?? 0) === 0 &&
|
|
1064
|
+
(bindings.dataSourceClassProps?.size ?? 0) === 0;
|
|
1065
|
+
if (receiver.type === "Identifier") {
|
|
1066
|
+
if (bindings.locals.has(receiver.name))
|
|
1067
|
+
return true;
|
|
1068
|
+
return noBindingsFound;
|
|
1069
|
+
}
|
|
1070
|
+
if (receiver.type === "MemberExpression" ||
|
|
1071
|
+
receiver.type === "OptionalMemberExpression") {
|
|
1072
|
+
return classifyMemberReceiver(receiver, bindings, noBindingsFound);
|
|
1073
|
+
}
|
|
1074
|
+
if (receiver.type === "CallExpression" ||
|
|
1075
|
+
receiver.type === "OptionalCallExpression") {
|
|
1076
|
+
if (isRepositoryReturningCall(receiver))
|
|
1077
|
+
return true;
|
|
1078
|
+
return noBindingsFound;
|
|
1079
|
+
}
|
|
1080
|
+
return noBindingsFound;
|
|
1081
|
+
};
|
|
1082
|
+
exports.isRepositoryReceiver = isRepositoryReceiver;
|
|
289
1083
|
//# sourceMappingURL=ast-helpers.js.map
|