uql-codemod 0.46.0 → 0.47.1
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.md +22 -0
- package/README.md +3 -2
- package/dist/bin.js +10 -3
- package/dist/cli.d.ts +5 -6
- package/dist/cli.js +9 -6
- package/dist/columnRefs.d.ts +7 -0
- package/dist/columnRefs.js +146 -0
- package/dist/context.d.ts +38 -0
- package/dist/context.js +44 -0
- package/dist/edits.d.ts +5 -6
- package/dist/edits.js +10 -6
- package/dist/entitySql.d.ts +5 -0
- package/dist/entitySql.js +11 -3
- package/dist/fieldType.d.ts +3 -9
- package/dist/fieldType.js +7 -15
- package/dist/imports.d.ts +40 -0
- package/dist/imports.js +314 -0
- package/dist/index.js +3 -1
- package/dist/keyMaps.d.ts +0 -4
- package/dist/keyMaps.js +2 -10
- package/dist/statementSql.d.ts +7 -0
- package/dist/statementSql.js +53 -0
- package/dist/syntax.d.ts +22 -0
- package/dist/syntax.js +41 -0
- package/dist/transform.d.ts +7 -2
- package/dist/transform.js +187 -579
- package/dist/tsconfig.d.ts +2 -5
- package/dist/tsconfig.js +3 -11
- package/package.json +1 -1
package/dist/transform.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { rewriteColumnRefs } from './columnRefs.js';
|
|
3
|
+
import { createContext, instanceTypeOf, memberNames, ownerOf } from './context.js';
|
|
4
|
+
import { appended, applyEdits, inserted, replaced } from './edits.js';
|
|
5
|
+
import { columnExpressions, handNamedColumns, rawTag, rawWhereEdit } from './entitySql.js';
|
|
4
6
|
import { fieldTypeFor, isBrandedString, relationTargetFor } from './fieldType.js';
|
|
5
|
-
import {
|
|
7
|
+
import { addImports, deadImportNames, dropDeadImports, renameExports, reportRemovedExports, uqlImport, uqlImportDeclarations, } from './imports.js';
|
|
8
|
+
import { entityGetterTarget, fieldKeysEdits, isCallback, keyListEdits, mappedByEdits, memberAccess, paramFor, quoted, referencesEdits, } from './keyMaps.js';
|
|
9
|
+
import { statementTemplate } from './statementSql.js';
|
|
10
|
+
import { arrayElements, decoratorName, decoratorsOf, identifierText, namedProperty, objectLiteral, objectProperties, propertyKey, propertyValue, } from './syntax.js';
|
|
6
11
|
const FIELD_DECORATORS = new Set(['Field', 'Id']);
|
|
7
12
|
const RELATION_DECORATORS = new Set(['OneToOne', 'ManyToOne', 'OneToMany', 'ManyToMany']);
|
|
8
13
|
const TO_ONE_DECORATORS = new Set(['OneToOne', 'ManyToOne']);
|
|
@@ -18,93 +23,9 @@ const REMOVED_DECORATORS = new Map([
|
|
|
18
23
|
['Transactional', 'wrap the body in `pool.transaction(async (querier) => { ... })`'],
|
|
19
24
|
['InjectQuerier', 'take the querier from the enclosing `pool.transaction()` callback'],
|
|
20
25
|
]);
|
|
21
|
-
/**
|
|
22
|
-
* Exports that no longer exist, mapped to what to do instead. Reported rather than rewritten, for the
|
|
23
|
-
* same reason as the decorators: what replaces each is decided at its call site.
|
|
24
|
-
*/
|
|
25
|
-
const REMOVED_EXPORTS = new Map([
|
|
26
|
-
[
|
|
27
|
-
'setQuerierPool',
|
|
28
|
-
'pass the pool where it is used: `createFetchHandler({ pool, include })`, `querierMiddleware({ pool, include })`',
|
|
29
|
-
],
|
|
30
|
-
['getQuerierPool', 'take the pool from the module that builds it, or from Nest DI'],
|
|
31
|
-
['getQuerier', 'use `pool.withQuerier(...)` / `pool.transaction(...)`, which release the connection'],
|
|
32
|
-
['ClientQuerierPool', "construct the `HttpQuerier` where it is used, `new HttpQuerier('/api')`"],
|
|
33
|
-
['QueryWhereFieldMap', 'use `QueryWhere`'],
|
|
34
|
-
['QueryStreamProjected', 'use `QueryProjected`'],
|
|
35
|
-
['RelationMappedBy', "a 'mappedBy' is `(keys: KeyMap<E>) => Key<E>` now"],
|
|
36
|
-
['RelationKeyMapper', 'it is `(keys: KeyMap<E>) => Key<E>` now'],
|
|
37
|
-
['augmentWhere', 'spread the two maps: `{ ...where, ...extra }`'],
|
|
38
|
-
['buildQueryWhereAsMap', 'a `$where` is a map already; name the key for ids: `{ id: [1, 2] }`'],
|
|
39
|
-
['AbstractPgQuerier', 'every pg-compatible pool returns `PgQuerier` (`uql-orm/postgres`), which is concrete'],
|
|
40
|
-
['AbstractHranaQuerierPool', 'extend `AbstractSqlQuerierPool` and hand every querier a `HranaQuerier`'],
|
|
41
|
-
[
|
|
42
|
-
'PreparedSqliteQuerier',
|
|
43
|
-
'use `SqliteQuerier`, which takes any driver that prepares, or extend `AbstractSqliteQuerier`',
|
|
44
|
-
],
|
|
45
|
-
['toSqliteBindValues', 'pass the values as they are: every SQLite driver binds a `SqliteBindValue[]`'],
|
|
46
|
-
['libsqlUseRemoteForMigrations', '`LibsqlQuerierPool.getMigrationQuerier()` decides it'],
|
|
47
|
-
['SqlCallback', 'use `EntitySql<E>`, which holds the callback form'],
|
|
48
|
-
[
|
|
49
|
-
'IndexColumnOptions',
|
|
50
|
-
"an entity's index entry is `EntityIndexColumnInput<E>`, the migration builder's `IndexColumnInput`",
|
|
51
|
-
],
|
|
52
|
-
['isKnownMigratorDialect', 'every dialect has a migrator, so it always held: drop the check'],
|
|
53
|
-
['QuerierPoolDialect', "read the pool's own: `P['dialect']`"],
|
|
54
|
-
['QuerierPoolQuerier', "read the pool's own: `Awaited<ReturnType<P['getQuerier']>>`"],
|
|
55
|
-
[
|
|
56
|
-
'POSTGRES_WIRE_DRIVER_CAPABILITIES',
|
|
57
|
-
'pass `driverCapabilities: { nativeArrays: false, explicitJsonCast: true }` to the dialect',
|
|
58
|
-
],
|
|
59
|
-
['MysqlLikeSqlDialect', 'extend `MySqlDialect` (`uql-orm/mysql`) or `MariaDialect` (`uql-orm/maria`)'],
|
|
60
|
-
['D1Meta', "uql reads `D1Result['meta']`; the rest of a binding is typed by `@cloudflare/workers-types`"],
|
|
61
|
-
['D1ExecResult', 'the rest of a binding is typed by `@cloudflare/workers-types`'],
|
|
62
|
-
['createSchemaGenerator', 'use `new SqlSchemaGenerator(dialect)`, or `migrator.getSchemaGenerator()`'],
|
|
63
|
-
]);
|
|
64
|
-
/**
|
|
65
|
-
* Exports renamed or moved and nothing else - the driver classes were empty subclasses - so the import and
|
|
66
|
-
* every use follow, the import moving to `from` where the name lives in another entry.
|
|
67
|
-
*/
|
|
68
|
-
const RENAMED_EXPORTS = new Map([
|
|
69
|
-
['QueryWhereMap', { to: 'QueryWhere' }],
|
|
70
|
-
['RelationKeyMap', { to: 'KeyMap' }],
|
|
71
|
-
['FilterCondition', { to: 'FilterWhere' }],
|
|
72
|
-
['PgDialect', { to: 'PostgresDialect', from: 'uql-orm/postgres' }],
|
|
73
|
-
['NeonDialect', { to: 'PostgresDialect', from: 'uql-orm/postgres' }],
|
|
74
|
-
['PgliteDialect', { to: 'PostgresDialect', from: 'uql-orm/postgres' }],
|
|
75
|
-
['CrdbQuerier', { to: 'PgQuerier', from: 'uql-orm/postgres' }],
|
|
76
|
-
['NeonQuerier', { to: 'PgQuerier', from: 'uql-orm/postgres' }],
|
|
77
|
-
['MySql2Dialect', { to: 'MySqlDialect', from: 'uql-orm/mysql' }],
|
|
78
|
-
['MongodbNativeDialect', { to: 'MongoDialect', from: 'uql-orm/mongo' }],
|
|
79
|
-
['LibsqlQuerier', { to: 'HranaQuerier', from: 'uql-orm/sqlite' }],
|
|
80
|
-
['TursoQuerier', { to: 'HranaQuerier', from: 'uql-orm/sqlite' }],
|
|
81
|
-
['TursoLocalQuerier', { to: 'SqliteQuerier', from: 'uql-orm/sqlite' }],
|
|
82
|
-
['TursoDatabase', { to: 'SqliteDatabase', from: 'uql-orm/sqlite' }],
|
|
83
|
-
['SqlMigrationModuleOptions', { to: 'MigrationModuleOptions' }],
|
|
84
|
-
['buildSqlQuerierMigrationModule', { to: 'buildMigrationModule' }],
|
|
85
|
-
['QueryDialect', { to: 'SqlQueryDialect' }],
|
|
86
|
-
['EngineFeatures', { to: 'DialectFeatures' }],
|
|
87
|
-
['KnownMigratorDialect', { to: 'DialectName', from: 'uql-orm' }],
|
|
88
|
-
['D1Preparer', { to: 'D1Queryable' }],
|
|
89
|
-
['D1Database', { to: 'D1Queryable', from: 'uql-orm/d1' }],
|
|
90
|
-
['MongoQuerier', { to: 'MongoQuerier', from: 'uql-orm/mongo' }],
|
|
91
|
-
['isMongoQuerier', { to: 'isMongoQuerier', from: 'uql-orm/mongo' }],
|
|
92
|
-
['MongoMigrationStorage', { to: 'MongoMigrationStorage', from: 'uql-orm/mongo' }],
|
|
93
|
-
['MongoSchemaIntrospector', { to: 'MongoSchemaIntrospector', from: 'uql-orm/mongo' }],
|
|
94
|
-
]);
|
|
95
|
-
/** A node's decorators, or none where it is a kind that cannot carry them. */
|
|
96
|
-
function decoratorsOf(node) {
|
|
97
|
-
return ts.canHaveDecorators(node) ? (ts.getDecorators(node) ?? []) : [];
|
|
98
|
-
}
|
|
99
|
-
function decoratorName(node) {
|
|
100
|
-
const call = ts.isCallExpression(node.expression) ? node.expression : undefined;
|
|
101
|
-
const target = call?.expression ?? node.expression;
|
|
102
|
-
return ts.isIdentifier(target) ? target.text : undefined;
|
|
103
|
-
}
|
|
26
|
+
/** A decorator's options; `@Field` uncalled is opaque, since adding a call would guess it is the factory. */
|
|
104
27
|
function decoratorOptions(node) {
|
|
105
28
|
if (!ts.isCallExpression(node.expression)) {
|
|
106
|
-
// `@Field` rather than `@Field()`: there is no argument list to write into, and adding one would mean
|
|
107
|
-
// guessing that this is the decorator factory the codemod thinks it is.
|
|
108
29
|
return { kind: 'opaque', reason: 'it is used without being called' };
|
|
109
30
|
}
|
|
110
31
|
const [first] = node.expression.arguments;
|
|
@@ -119,18 +40,9 @@ function optionsOf(value) {
|
|
|
119
40
|
? { kind: 'opaque', reason: 'its options object spreads another' }
|
|
120
41
|
: { kind: 'literal', node: value };
|
|
121
42
|
}
|
|
122
|
-
/**
|
|
123
|
-
* The property `name` in the options, whether its key is plain, quoted or a shorthand.
|
|
124
|
-
*
|
|
125
|
-
* All three forms, because both callers care about the option being *there*: one to leave an option
|
|
126
|
-
* alone that is already stated, the other to rename it. Matching only the plain key inserted a second
|
|
127
|
-
* `type` beside a quoted one, and left a shorthand `virtual` behind while reporting nothing.
|
|
128
|
-
*/
|
|
43
|
+
/** The option `name`, however its key is written: a stated option is left alone, and a renamed one moved. */
|
|
129
44
|
function findProperty(options, name) {
|
|
130
|
-
|
|
131
|
-
return undefined;
|
|
132
|
-
}
|
|
133
|
-
return options.node.properties.find((prop) => (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) && propertyKey(prop.name) === name);
|
|
45
|
+
return options.kind === 'literal' ? namedProperty(options.node, name) : undefined;
|
|
134
46
|
}
|
|
135
47
|
/**
|
|
136
48
|
* Renames the option `from` to `to`. Only the key is replaced, so the value, its formatting and any comment
|
|
@@ -143,7 +55,7 @@ function renameOption(options, from, to, node, ctx) {
|
|
|
143
55
|
return;
|
|
144
56
|
}
|
|
145
57
|
if (findProperty(options, to)) {
|
|
146
|
-
ctx.
|
|
58
|
+
ctx.report(node, `gives both '${from}' and '${to}'; keep '${to}'.`);
|
|
147
59
|
return;
|
|
148
60
|
}
|
|
149
61
|
ctx.edits.push(replaced(property.name, ts.isShorthandPropertyAssignment(property) ? `${to}: ${from}` : to));
|
|
@@ -161,25 +73,12 @@ function noteHandNamedColumns(sql, owner, ctx) {
|
|
|
161
73
|
if (!sql || !names.length) {
|
|
162
74
|
return;
|
|
163
75
|
}
|
|
164
|
-
ctx.
|
|
165
|
-
`
|
|
166
|
-
}
|
|
167
|
-
/** The names of the members of the entity `node` declares or names. */
|
|
168
|
-
function memberNames(node, checker) {
|
|
169
|
-
return propertyNames(instanceTypeOf(node, checker));
|
|
170
|
-
}
|
|
171
|
-
/** The instance type of the entity `node` declares or names. */
|
|
172
|
-
function instanceTypeOf(node, checker) {
|
|
173
|
-
const type = checker.getTypeAtLocation(node);
|
|
174
|
-
return type.getConstructSignatures()[0]?.getReturnType() ?? type;
|
|
175
|
-
}
|
|
176
|
-
function propertyNames(type) {
|
|
177
|
-
return new Set(type.getProperties().map(({ name }) => name));
|
|
76
|
+
ctx.note(sql, `SQL names ${names.map((name) => `'${name}'`).join(', ')} by hand, which a rename does not reach; ` +
|
|
77
|
+
`read each off a callback's refs instead: (${owner.param}) => raw\`...\${${owner.param}.<member>}...\``);
|
|
178
78
|
}
|
|
179
|
-
/** Inserts `option` into the decorator's options object, creating one
|
|
79
|
+
/** Inserts `option` into the decorator's options object, creating one in an empty argument list. */
|
|
180
80
|
function insertOption(options, option) {
|
|
181
81
|
if (options.kind === 'empty') {
|
|
182
|
-
// `@Field()` -> `@Field({ ... })`: the argument list is empty, so this only ever inserts.
|
|
183
82
|
const { arguments: args } = options.call;
|
|
184
83
|
return { start: args.pos, end: args.end, text: `{ ${option} }` };
|
|
185
84
|
}
|
|
@@ -187,11 +86,8 @@ function insertOption(options, option) {
|
|
|
187
86
|
return first ? inserted(first, `${option}, `) : replaced(options.node, `{ ${option} }`);
|
|
188
87
|
}
|
|
189
88
|
/**
|
|
190
|
-
* Writes one option the decorator can no longer infer at runtime, or records why it could not.
|
|
191
|
-
*
|
|
192
|
-
* Returns the property's type when an edit was written, so a caller can say something about what was
|
|
193
|
-
* just decided. Nothing is written for options the codemod cannot read: replacing them would drop
|
|
194
|
-
* whatever the author put there.
|
|
89
|
+
* Writes one option the decorator can no longer infer at runtime, or records why it could not. Returns the
|
|
90
|
+
* property's type where it wrote one, for the caller to say more about.
|
|
195
91
|
*/
|
|
196
92
|
function addInferredOption(decorator, node, ctx, spec) {
|
|
197
93
|
const options = decoratorOptions(decorator);
|
|
@@ -199,34 +95,32 @@ function addInferredOption(decorator, node, ctx, spec) {
|
|
|
199
95
|
return undefined;
|
|
200
96
|
}
|
|
201
97
|
if (options.kind === 'opaque') {
|
|
202
|
-
ctx.
|
|
98
|
+
ctx.report(node, `cannot add '${spec.option}' because ${options.reason}`);
|
|
203
99
|
return undefined;
|
|
204
100
|
}
|
|
205
101
|
const type = ctx.checker.getTypeAtLocation(node);
|
|
206
102
|
const value = spec.infer(type);
|
|
207
103
|
if (!value) {
|
|
208
|
-
ctx.
|
|
104
|
+
ctx.report(node, `cannot infer '${spec.option}' for ${ctx.checker.typeToString(type)}`);
|
|
209
105
|
return undefined;
|
|
210
106
|
}
|
|
211
107
|
ctx.edits.push(insertOption(options, `${spec.option}: ${value}`));
|
|
212
108
|
return type;
|
|
213
109
|
}
|
|
214
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* Writes the `type` that `design:type` supplied, unless `references` (the key it points at decides) or
|
|
112
|
+
* `computed` does. A branded id erased to `String`, which keeps the column; `'uuid'` would change it, so that
|
|
113
|
+
* is noted rather than decided.
|
|
114
|
+
*/
|
|
215
115
|
function addFieldType(decorator, node, ctx) {
|
|
216
116
|
const type = addInferredOption(decorator, node, ctx, {
|
|
217
117
|
option: 'type',
|
|
218
|
-
|
|
219
|
-
// points at, which also carries that key's `columnType` and length.
|
|
220
|
-
satisfiedBy: ['type', 'references'],
|
|
118
|
+
satisfiedBy: ['type', 'references', 'computed'],
|
|
221
119
|
infer: fieldTypeFor,
|
|
222
120
|
});
|
|
223
|
-
// A branded id type (`type UUID = `${string}-${string}-...``) erased to `String` under reflection, so
|
|
224
|
-
// `String` is what keeps the existing column. It is often not what was wanted, though, and switching
|
|
225
|
-
// to `'uuid'` changes the generated column, so say so rather than decide.
|
|
226
121
|
if (type && isBrandedString(type)) {
|
|
227
|
-
ctx.
|
|
228
|
-
|
|
229
|
-
"'uuid', change it deliberately: that alters the schema.");
|
|
122
|
+
ctx.note(node, `set 'type: ${fieldTypeFor(type)}' for ${ctx.checker.typeToString(type)}, matching the previous behaviour. ` +
|
|
123
|
+
"If the column should be 'uuid', change it deliberately: that alters the schema.");
|
|
230
124
|
}
|
|
231
125
|
}
|
|
232
126
|
/** Writes the `entity` getter relations can no longer infer. */
|
|
@@ -246,10 +140,9 @@ function addRelationEntity(decorator, node, ctx) {
|
|
|
246
140
|
* is declared, or `defineEntity`'s `fields`. A column it cannot see declared is reported instead.
|
|
247
141
|
*/
|
|
248
142
|
function addForeignKeyReference(relation, key, owner, ctx, site) {
|
|
249
|
-
const options = { kind: 'literal', node: relation };
|
|
250
143
|
const cardinality = propertyValue(relation, 'cardinality');
|
|
251
144
|
const toOne = !cardinality || (ts.isStringLiteralLike(cardinality) && TO_ONE_CARDINALITIES.has(cardinality.text));
|
|
252
|
-
const joined = ['mappedBy', 'through', 'references'].some((option) =>
|
|
145
|
+
const joined = ['mappedBy', 'through', 'references'].some((option) => namedProperty(relation, option));
|
|
253
146
|
const last = relation.properties.at(-1);
|
|
254
147
|
if (!key || !last || !toOne || joined) {
|
|
255
148
|
return;
|
|
@@ -257,7 +150,7 @@ function addForeignKeyReference(relation, key, owner, ctx, site) {
|
|
|
257
150
|
const column = `${key}Id`;
|
|
258
151
|
const declaration = columnDeclaration(column, relation, owner, ctx.checker, site);
|
|
259
152
|
if (typeof declaration === 'string') {
|
|
260
|
-
ctx.
|
|
153
|
+
ctx.report(site && ts.isPropertyDeclaration(site) ? site : relation, declaration);
|
|
261
154
|
return;
|
|
262
155
|
}
|
|
263
156
|
if (declaration) {
|
|
@@ -279,7 +172,7 @@ function columnDeclaration(column, relation, owner, checker, site) {
|
|
|
279
172
|
const declared = site ? objectProperties(site).some(({ name }) => propertyKey(name) === column) : member;
|
|
280
173
|
return declared ? undefined : undeclaredColumn(column);
|
|
281
174
|
}
|
|
282
|
-
/** `@Field({ references: () => Target }) <column>?: <key type
|
|
175
|
+
/** `@Field({ references: () => Target }) <column>?: <key type> | null;` ahead of the relation, or why it cannot be written. */
|
|
283
176
|
function foreignKeyColumn(property, column, relation, checker) {
|
|
284
177
|
const targetType = checker.getNonNullableType(checker.getTypeAtLocation(property));
|
|
285
178
|
const target = entityGetterTarget(relation) ?? relationTargetFor(targetType, checker);
|
|
@@ -291,10 +184,12 @@ function foreignKeyColumn(property, column, relation, checker) {
|
|
|
291
184
|
if (!target || !key) {
|
|
292
185
|
return undeclaredColumn(column);
|
|
293
186
|
}
|
|
294
|
-
const indent = ' '.repeat(property.getSourceFile().getLineAndCharacterOfPosition(property.getStart()).character);
|
|
295
187
|
const type = keyTypeSource(key, property, checker);
|
|
296
|
-
|
|
297
|
-
|
|
188
|
+
return inserted(property, `@Field({ references: () => ${target} }) ${column}?: ${type} | null;\n${indentOf(property)}`);
|
|
189
|
+
}
|
|
190
|
+
/** The whitespace a line starting at `node` is indented by. */
|
|
191
|
+
function indentOf(node) {
|
|
192
|
+
return ' '.repeat(node.getSourceFile().getLineAndCharacterOfPosition(node.getStart()).character);
|
|
298
193
|
}
|
|
299
194
|
/** The key's type as written where that is in scope, the same file, or as the checker spells it out. */
|
|
300
195
|
function keyTypeSource(key, at, checker) {
|
|
@@ -322,7 +217,7 @@ function rewriteValue(value, rewrite, node, advice, ctx) {
|
|
|
322
217
|
ctx.edits.push(...edits);
|
|
323
218
|
}
|
|
324
219
|
else {
|
|
325
|
-
ctx.
|
|
220
|
+
ctx.report(node, `${advice}; this one could not be read`);
|
|
326
221
|
}
|
|
327
222
|
}
|
|
328
223
|
function rewriteKeyList(value, param, node, what, ctx) {
|
|
@@ -333,21 +228,22 @@ function rewriteKeyList(value, param, node, what, ctx) {
|
|
|
333
228
|
* members named by string read off a callback's parameter named after the class, and SQL written as it is taken.
|
|
334
229
|
*/
|
|
335
230
|
function rewriteClassDecorators(node, ctx) {
|
|
336
|
-
const owner =
|
|
231
|
+
const owner = ownerOf(node);
|
|
337
232
|
for (const decorator of decoratorsOf(node)) {
|
|
338
233
|
if (!ts.isCallExpression(decorator.expression)) {
|
|
339
234
|
continue;
|
|
340
235
|
}
|
|
341
236
|
const [first, second] = decorator.expression.arguments;
|
|
342
237
|
const name = decoratorName(decorator);
|
|
238
|
+
const options = objectLiteral(first);
|
|
343
239
|
if (name === 'Index') {
|
|
344
240
|
rewriteIndex(first, second, owner, decorator, "'@Index' columns", ctx);
|
|
345
241
|
}
|
|
346
242
|
else if (name === 'Filter' && second) {
|
|
347
243
|
renameOption(optionsOf(second), 'condition', 'where', decorator, ctx);
|
|
348
244
|
}
|
|
349
|
-
else if (name === 'Entity' &&
|
|
350
|
-
rewriteEntityOptions(
|
|
245
|
+
else if (name === 'Entity' && options) {
|
|
246
|
+
rewriteEntityOptions(options, owner, ctx);
|
|
351
247
|
}
|
|
352
248
|
}
|
|
353
249
|
}
|
|
@@ -357,18 +253,20 @@ function rewriteClassDecorators(node, ctx) {
|
|
|
357
253
|
* Only uql's shapes, a class and a literal options object, are read.
|
|
358
254
|
*/
|
|
359
255
|
function rewriteDefineCall(call, ctx) {
|
|
360
|
-
const name =
|
|
256
|
+
const name = identifierText(call.expression);
|
|
361
257
|
const [entity = call, second, third] = call.arguments;
|
|
362
|
-
const owner =
|
|
363
|
-
|
|
364
|
-
|
|
258
|
+
const owner = ownerOf(entity);
|
|
259
|
+
const options = objectLiteral(second);
|
|
260
|
+
const relation = objectLiteral(third);
|
|
261
|
+
if (name === 'defineEntity' && options) {
|
|
262
|
+
rewriteEntityOptions(options, owner, ctx);
|
|
365
263
|
}
|
|
366
|
-
else if (name === 'defineIndex' &&
|
|
367
|
-
rewriteIndexOptions(
|
|
264
|
+
else if (name === 'defineIndex' && options) {
|
|
265
|
+
rewriteIndexOptions(options, owner, ctx);
|
|
368
266
|
}
|
|
369
|
-
else if (name === 'defineRelation' &&
|
|
370
|
-
rewriteRelationOptions(
|
|
371
|
-
addForeignKeyReference(
|
|
267
|
+
else if (name === 'defineRelation' && relation) {
|
|
268
|
+
rewriteRelationOptions(relation, owner.param, ctx);
|
|
269
|
+
addForeignKeyReference(relation, second && ts.isStringLiteralLike(second) ? second.text : undefined, owner, ctx);
|
|
372
270
|
}
|
|
373
271
|
else if (name === 'defineFilter' && third) {
|
|
374
272
|
renameOption(optionsOf(third), 'condition', 'where', call, ctx);
|
|
@@ -394,11 +292,10 @@ function rewriteEntityOptions(options, owner, ctx) {
|
|
|
394
292
|
for (const hook of objectProperties(propertyValue(options, 'hooks'))) {
|
|
395
293
|
rewriteKeyList(hook.initializer, owner.param, hook, 'a hook list', ctx);
|
|
396
294
|
}
|
|
397
|
-
const declared = fields && ts.isObjectLiteralExpression(fields) ? fields : undefined;
|
|
398
295
|
for (const relation of objectProperties(propertyValue(options, 'relations'))) {
|
|
399
296
|
if (ts.isObjectLiteralExpression(relation.initializer)) {
|
|
400
297
|
rewriteRelationOptions(relation.initializer, owner.param, ctx);
|
|
401
|
-
addForeignKeyReference(relation.initializer, propertyKey(relation.name), owner, ctx,
|
|
298
|
+
addForeignKeyReference(relation.initializer, propertyKey(relation.name), owner, ctx, objectLiteral(fields));
|
|
402
299
|
}
|
|
403
300
|
}
|
|
404
301
|
}
|
|
@@ -410,7 +307,7 @@ function rewriteIndexOptions(index, owner, ctx) {
|
|
|
410
307
|
* `include` read off the entity's refs, its `where` written as {@link rewriteIndexWhere} writes it, its SQL noted.
|
|
411
308
|
*/
|
|
412
309
|
function rewriteIndex(columns, options, owner, node, what, ctx) {
|
|
413
|
-
const literal =
|
|
310
|
+
const literal = objectLiteral(options);
|
|
414
311
|
rewriteKeyList(columns, owner.param, node, what, ctx);
|
|
415
312
|
rewriteKeyList(literal && propertyValue(literal, 'include'), owner.param, node, "'include'", ctx);
|
|
416
313
|
rewriteIndexWhere(literal, ctx);
|
|
@@ -418,10 +315,7 @@ function rewriteIndex(columns, options, owner, node, what, ctx) {
|
|
|
418
315
|
noteHandNamedColumns(sql, owner, ctx);
|
|
419
316
|
}
|
|
420
317
|
}
|
|
421
|
-
/**
|
|
422
|
-
* A partial-index `where` string as `raw`, on an entity and in the migration builder alike. A string it
|
|
423
|
-
* cannot write as `raw` is reported.
|
|
424
|
-
*/
|
|
318
|
+
/** A partial-index `where` string as `raw`, on an entity and in the migration builder alike, or reported. */
|
|
425
319
|
function rewriteIndexWhere(options, ctx) {
|
|
426
320
|
const where = options && propertyValue(options, 'where');
|
|
427
321
|
if (!where || !isStringTyped(ctx.checker.getTypeAtLocation(where))) {
|
|
@@ -433,13 +327,78 @@ function rewriteIndexWhere(options, ctx) {
|
|
|
433
327
|
ctx.imports.set('raw', 'the raw`...`');
|
|
434
328
|
}
|
|
435
329
|
else {
|
|
436
|
-
ctx.
|
|
330
|
+
ctx.report(where, "write the partial-index 'where' as raw`...` or a predicate; this one could not be read");
|
|
437
331
|
}
|
|
438
332
|
}
|
|
439
333
|
/** Whether every value `type` admits is a string. */
|
|
440
334
|
function isStringTyped(type) {
|
|
441
335
|
return type.isUnion() ? type.types.every(isStringTyped) : Boolean(type.flags & ts.TypeFlags.StringLike);
|
|
442
336
|
}
|
|
337
|
+
/** The name of the method `call` calls where uql-orm declares it, so a `run` of another library's is left alone. */
|
|
338
|
+
function uqlMethod(call, ctx) {
|
|
339
|
+
const callee = call.expression;
|
|
340
|
+
if (!ts.isPropertyAccessExpression(callee)) {
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
const declarations = ctx.checker.getSymbolAtLocation(callee.name)?.declarations ?? [];
|
|
344
|
+
const declared = declarations.some((it) => it.getSourceFile().fileName.includes('/node_modules/uql-orm/'));
|
|
345
|
+
return declared ? callee.name.text : undefined;
|
|
346
|
+
}
|
|
347
|
+
/** A uql method whose shape or meaning changed: a querier's `run`/`all`, and `Migrator`'s `pending`, `executed`, `down`. */
|
|
348
|
+
function rewriteMethodCall(call, ctx) {
|
|
349
|
+
const method = uqlMethod(call, ctx);
|
|
350
|
+
if (method === 'run' || method === 'all') {
|
|
351
|
+
rewriteSqlCall(call, method, ctx);
|
|
352
|
+
}
|
|
353
|
+
else if (method === 'pending' || method === 'executed') {
|
|
354
|
+
ctx.report(call, `'${method}()' was removed; read \`(await migrator.status()).${method}\``);
|
|
355
|
+
}
|
|
356
|
+
else if (method === 'down' && !call.arguments.length) {
|
|
357
|
+
ctx.note(call, 'down() with no options reverts only the last migration now; pass `{ step: Infinity }` to revert them all');
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* A querier's `run`/`all` handed SQL as a string, which now takes a tagged statement: a literal becomes
|
|
362
|
+
* `run`...``, its values interpolated where {@link statementTemplate} can place them, and SQL built at run
|
|
363
|
+
* time `raw.text`, which splices as the string did. Values it cannot place are reported.
|
|
364
|
+
*/
|
|
365
|
+
function rewriteSqlCall(call, method, ctx) {
|
|
366
|
+
const [sql, ...values] = call.arguments;
|
|
367
|
+
if (!sql || !isStringTyped(ctx.checker.getTypeAtLocation(sql))) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const template = statementTemplate(sql, values);
|
|
371
|
+
if (template) {
|
|
372
|
+
const typeArguments = call.typeArguments ? `<${call.typeArguments.map((type) => type.getText()).join(', ')}>` : '';
|
|
373
|
+
ctx.edits.push(replaced(call, `${call.expression.getText()}${typeArguments}${template}`));
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (values.length) {
|
|
377
|
+
ctx.report(call, `${method}() takes one raw statement: write the values into it, raw\`... \${value}\`, which binds them`);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (ts.isTemplateExpression(sql)) {
|
|
381
|
+
ctx.note(call, 'the values in this template are spliced into the SQL; write it as raw`...` to bind them');
|
|
382
|
+
}
|
|
383
|
+
ctx.edits.push(replaced(sql, `raw.text(${sql.getText()})`));
|
|
384
|
+
ctx.imports.set('raw', 'the raw.text(...)');
|
|
385
|
+
}
|
|
386
|
+
/** `const { id } = await q.upsertOne(...)`, which resolves to the id now, as the id: other keys are reported. */
|
|
387
|
+
function rewriteUpsertResult(declaration, ctx) {
|
|
388
|
+
const { name, initializer } = declaration;
|
|
389
|
+
const call = initializer && ts.isAwaitExpression(initializer) ? initializer.expression : initializer;
|
|
390
|
+
if (!ts.isObjectBindingPattern(name) || !call || !ts.isCallExpression(call) || uqlMethod(call, ctx) !== 'upsertOne') {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const [element] = name.elements;
|
|
394
|
+
const key = element && (element.propertyName ?? element.name);
|
|
395
|
+
if (name.elements.length === 1 && key && identifierText(key) === 'id' && ts.isIdentifier(element.name)) {
|
|
396
|
+
ctx.edits.push(replaced(name, element.name.text));
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
ctx.report(declaration, "upsertOne() resolves to the id; 'created' and 'changes' are gone");
|
|
400
|
+
}
|
|
401
|
+
}
|
|
443
402
|
/** The migration builder's index methods, each with the position of the options it takes. */
|
|
444
403
|
const BUILDER_INDEX_METHODS = new Map([
|
|
445
404
|
['index', 1],
|
|
@@ -447,22 +406,22 @@ const BUILDER_INDEX_METHODS = new Map([
|
|
|
447
406
|
['addIndex', 1],
|
|
448
407
|
['createIndex', 2],
|
|
449
408
|
]);
|
|
450
|
-
/** The migration builder's `expr` helpers
|
|
451
|
-
const EXPR_VALUES = new
|
|
409
|
+
/** The migration builder's `expr` helpers an entity imports as values under the same name, `now` renamed. */
|
|
410
|
+
const EXPR_VALUES = new Map([
|
|
411
|
+
['now', 'currentTimestamp'],
|
|
412
|
+
...['currentDate', 'currentTime', 'uuid', 'uuidv7'].map((name) => [name, name]),
|
|
413
|
+
]);
|
|
452
414
|
/**
|
|
453
415
|
* Rewrites the migration builder's `expr.<helper>()` default as the value or `raw` an entity declares, or reports
|
|
454
416
|
* one with no rewrite. Called only where the file imports `expr` from uql, so a variable of its own is left alone.
|
|
455
417
|
*/
|
|
456
418
|
function rewriteExprCall(call, ctx) {
|
|
457
419
|
const callee = call.expression;
|
|
458
|
-
if (!ts.isPropertyAccessExpression(callee) ||
|
|
459
|
-
!ts.isIdentifier(callee.expression) ||
|
|
460
|
-
callee.expression.text !== 'expr') {
|
|
420
|
+
if (!ts.isPropertyAccessExpression(callee) || identifierText(callee.expression) !== 'expr') {
|
|
461
421
|
return;
|
|
462
422
|
}
|
|
463
423
|
const helper = callee.name.text;
|
|
464
|
-
|
|
465
|
-
const value = helper === 'now' ? 'currentTimestamp' : EXPR_VALUES.has(helper) ? helper : undefined;
|
|
424
|
+
const value = EXPR_VALUES.get(helper);
|
|
466
425
|
const [sql] = call.arguments;
|
|
467
426
|
if (value) {
|
|
468
427
|
ctx.edits.push(replaced(call, value));
|
|
@@ -473,7 +432,7 @@ function rewriteExprCall(call, ctx) {
|
|
|
473
432
|
ctx.imports.set('raw', 'the raw`...`');
|
|
474
433
|
}
|
|
475
434
|
else {
|
|
476
|
-
ctx.
|
|
435
|
+
ctx.report(call, `expr.${helper}() is gone: declare the column with m.raw(...), or use a stamp on the entity`);
|
|
477
436
|
return;
|
|
478
437
|
}
|
|
479
438
|
ctx.rewritten.expr += 1;
|
|
@@ -483,11 +442,9 @@ function rewriteBuilderCall(call, ctx) {
|
|
|
483
442
|
const at = ts.isPropertyAccessExpression(call.expression)
|
|
484
443
|
? BUILDER_INDEX_METHODS.get(call.expression.name.text)
|
|
485
444
|
: undefined;
|
|
486
|
-
if (at
|
|
487
|
-
|
|
445
|
+
if (at !== undefined) {
|
|
446
|
+
rewriteIndexWhere(objectLiteral(call.arguments[at]), ctx);
|
|
488
447
|
}
|
|
489
|
-
const options = call.arguments[at];
|
|
490
|
-
rewriteIndexWhere(options && ts.isObjectLiteralExpression(options) ? options : undefined, ctx);
|
|
491
448
|
}
|
|
492
449
|
const UPSERT_CALLS = new Set(['upsertOne', 'upsertMany', 'upsertInto']);
|
|
493
450
|
/**
|
|
@@ -497,22 +454,18 @@ const UPSERT_CALLS = new Set(['upsertOne', 'upsertMany', 'upsertInto']);
|
|
|
497
454
|
*/
|
|
498
455
|
function rewriteUpsertCall(call, ctx) {
|
|
499
456
|
const callee = call.expression;
|
|
500
|
-
const name = ts.isPropertyAccessExpression(callee) ? callee.name : callee;
|
|
457
|
+
const name = identifierText(ts.isPropertyAccessExpression(callee) ? callee.name : callee);
|
|
501
458
|
const options = call.arguments[3];
|
|
502
459
|
const update = options && upsertUpdate(options);
|
|
503
|
-
if (
|
|
460
|
+
if (name && UPSERT_CALLS.has(name) && update) {
|
|
504
461
|
ctx.edits.push(replaced(options, update.getText()));
|
|
505
462
|
}
|
|
506
463
|
}
|
|
507
464
|
/** The value of options holding `update` alone, `{ update: value }` or `{ update }`. */
|
|
508
465
|
function upsertUpdate(options) {
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
}
|
|
513
|
-
return property && ts.isShorthandPropertyAssignment(property) && property.name.text === 'update'
|
|
514
|
-
? property.name
|
|
515
|
-
: undefined;
|
|
466
|
+
const literal = objectLiteral(options);
|
|
467
|
+
const property = literal?.properties.length === 1 ? namedProperty(literal, 'update') : undefined;
|
|
468
|
+
return property && (ts.isPropertyAssignment(property) ? property.initializer : property.name);
|
|
516
469
|
}
|
|
517
470
|
/**
|
|
518
471
|
* `owner` is the declaring class's parameter and `target` the related one's, read off the `entity`
|
|
@@ -560,7 +513,7 @@ function isTextSearch(node) {
|
|
|
560
513
|
}
|
|
561
514
|
function rewriteAggregate(agg, ctx) {
|
|
562
515
|
if (ts.isObjectLiteralExpression(agg.parent) && propertyValue(agg.parent, '$select')) {
|
|
563
|
-
ctx.
|
|
516
|
+
ctx.report(agg, "merge '$agg' into the '$select' beside it");
|
|
564
517
|
return;
|
|
565
518
|
}
|
|
566
519
|
ctx.edits.push(replaced(agg.name, '$select'));
|
|
@@ -573,29 +526,15 @@ function rewriteAggregate(agg, ctx) {
|
|
|
573
526
|
}
|
|
574
527
|
}
|
|
575
528
|
}
|
|
576
|
-
/** The property assignments of an object literal, or none where `node` is not one. */
|
|
577
|
-
function objectProperties(node) {
|
|
578
|
-
return node && ts.isObjectLiteralExpression(node) ? node.properties.filter(ts.isPropertyAssignment) : [];
|
|
579
|
-
}
|
|
580
|
-
/** The elements of an array literal, or none where `node` is not one. */
|
|
581
|
-
function arrayElements(node) {
|
|
582
|
-
return node && ts.isArrayLiteralExpression(node) ? node.elements : [];
|
|
583
|
-
}
|
|
584
529
|
/**
|
|
585
|
-
* Drops `declare` from a decorated field
|
|
586
|
-
* nothing to decorate and rejects it ("Decorators are not valid here").
|
|
587
|
-
* because they only ever reached the class through `target.constructor`.
|
|
530
|
+
* Drops `declare` from a decorated field, with the whitespace after it: a `declare` field emits nothing, so
|
|
531
|
+
* the standard spec has nothing to decorate and rejects it ("Decorators are not valid here").
|
|
588
532
|
*/
|
|
589
533
|
function dropDeclare(node, ctx) {
|
|
590
534
|
const modifier = node.modifiers?.find((m) => m.kind === ts.SyntaxKind.DeclareKeyword);
|
|
591
535
|
if (modifier) {
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
const text = node.getSourceFile().text;
|
|
595
|
-
let end = modifier.getEnd();
|
|
596
|
-
while (/\s/.test(text[end])) {
|
|
597
|
-
end += 1;
|
|
598
|
-
}
|
|
536
|
+
const rest = node.getSourceFile().text.slice(modifier.getEnd());
|
|
537
|
+
const end = modifier.getEnd() + rest.length - rest.trimStart().length;
|
|
599
538
|
ctx.edits.push({ start: modifier.getStart(), end, text: '' });
|
|
600
539
|
}
|
|
601
540
|
}
|
|
@@ -608,85 +547,23 @@ function unwrapRelationAlias(node, ctx) {
|
|
|
608
547
|
const declared = node.type;
|
|
609
548
|
if (declared &&
|
|
610
549
|
ts.isTypeReferenceNode(declared) &&
|
|
611
|
-
|
|
612
|
-
declared.typeName.text === 'Relation' &&
|
|
550
|
+
identifierText(declared.typeName) === 'Relation' &&
|
|
613
551
|
declared.typeArguments?.length === 1) {
|
|
614
552
|
ctx.edits.push(replaced(declared, declared.typeArguments[0].getText()));
|
|
615
553
|
ctx.rewritten.Relation += 1;
|
|
616
554
|
}
|
|
617
555
|
}
|
|
618
|
-
/** Removes a statement together with the rest of its line, so nothing is left blank behind it. */
|
|
619
|
-
function removeStatement(node) {
|
|
620
|
-
const source = node.getSourceFile();
|
|
621
|
-
const line = source.getLineAndCharacterOfPosition(node.getEnd()).line;
|
|
622
|
-
const nextLineStart = source.getLineStarts()[line + 1];
|
|
623
|
-
return { start: node.getStart(), end: nextLineStart ?? node.getEnd(), text: '' };
|
|
624
|
-
}
|
|
625
|
-
/**
|
|
626
|
-
* Names `uql-orm` no longer exports and that the codemod does remove: `Relation` once every usage was
|
|
627
|
-
* unwrapped, and `expr` once every default was rewritten. A removed *decorator* keeps its import on purpose (see
|
|
628
|
-
* {@link REMOVED_DECORATORS}).
|
|
629
|
-
*/
|
|
630
|
-
function deadImportNames(source, ctx) {
|
|
631
|
-
const imported = uqlImports(source, true);
|
|
632
|
-
// Every use of the name the file imports, the places no rewrite reaches included.
|
|
633
|
-
const uses = (name) => {
|
|
634
|
-
const element = imported.find((specifier) => importedName(specifier) === name);
|
|
635
|
-
return element ? usesOf(source, element.name, ctx.checker).length : 0;
|
|
636
|
-
};
|
|
637
|
-
const { rewritten } = ctx;
|
|
638
|
-
const relations = uses('Relation');
|
|
639
|
-
if (relations > rewritten.Relation) {
|
|
640
|
-
ctx.unresolved.push(`${source.fileName}: ${relations - rewritten.Relation} 'Relation<T>' reference(s) are somewhere this codemod ` +
|
|
641
|
-
'does not rewrite; unwrap them and drop the import by hand');
|
|
642
|
-
}
|
|
643
|
-
const names = ['Relation', 'expr'];
|
|
644
|
-
return new Set(names.filter((name) => rewritten[name] > 0 && rewritten[name] === uses(name)));
|
|
645
|
-
}
|
|
646
|
-
/**
|
|
647
|
-
* Removes `import 'reflect-metadata'`, since the polyfill only ever fed `design:type`, and each `dead` name
|
|
648
|
-
* from the imports {@link renameExports} left as written, having left the dead names out of the rest.
|
|
649
|
-
*/
|
|
650
|
-
function dropDeadImports(source, dead, rewritten, ctx) {
|
|
651
|
-
for (const statement of source.statements) {
|
|
652
|
-
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
653
|
-
continue;
|
|
654
|
-
}
|
|
655
|
-
if (statement.moduleSpecifier.text === 'reflect-metadata' && !statement.importClause) {
|
|
656
|
-
ctx.edits.push(removeStatement(statement));
|
|
657
|
-
continue;
|
|
658
|
-
}
|
|
659
|
-
const named = statement.importClause?.namedBindings;
|
|
660
|
-
if (!named || !ts.isNamedImports(named) || rewritten.has(statement)) {
|
|
661
|
-
continue;
|
|
662
|
-
}
|
|
663
|
-
const dropped = named.elements.filter((element) => dead.has(element.name.text));
|
|
664
|
-
if (dropped.length === named.elements.length && !statement.importClause?.name) {
|
|
665
|
-
ctx.edits.push(removeStatement(statement));
|
|
666
|
-
continue;
|
|
667
|
-
}
|
|
668
|
-
for (const element of dropped) {
|
|
669
|
-
ctx.edits.push(removeFromList(named.elements, element, source));
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
556
|
/** The names a key is read from without help, mirroring `NamedIdKey` in `uql-orm`. */
|
|
674
557
|
const CONVENTIONAL_ID_NAMES = new Set(['id', '_id', 'uuid']);
|
|
675
558
|
/** Whether the member is an `[idKey]?: ...` brand, matched by the name it is written under. */
|
|
676
559
|
function isIdKeyBrand(member) {
|
|
677
560
|
return (ts.isPropertyDeclaration(member) &&
|
|
678
561
|
ts.isComputedPropertyName(member.name) &&
|
|
679
|
-
|
|
680
|
-
member.name.expression.text === 'idKey');
|
|
562
|
+
identifierText(member.name.expression) === 'idKey');
|
|
681
563
|
}
|
|
682
564
|
/**
|
|
683
|
-
* Names the class's key with the `idKey` brand
|
|
684
|
-
*
|
|
685
|
-
* Only the class's own `@Id` properties are read, which is also what makes an overriding key right:
|
|
686
|
-
* a subclass that replaces an inherited `id` needs the brand precisely because the inherited name
|
|
687
|
-
* would otherwise be taken for the key.
|
|
688
|
-
*
|
|
689
|
-
* One written makes `idKey` a value the file has to import, which is recorded for {@link addImports}.
|
|
565
|
+
* Names the class's key with the `idKey` brand where a conventional name does not. Only the class's own `@Id`
|
|
566
|
+
* properties are read: a subclass replacing an inherited `id` needs the brand, or that name is taken for the key.
|
|
690
567
|
*/
|
|
691
568
|
function brandIdKey(node, ctx) {
|
|
692
569
|
const ids = node.members.filter(isIdProperty);
|
|
@@ -695,65 +572,43 @@ function brandIdKey(node, ctx) {
|
|
|
695
572
|
}
|
|
696
573
|
const names = ids.map((m) => propertyKey(m.name)).filter((name) => name !== undefined);
|
|
697
574
|
if (names.length !== ids.length) {
|
|
698
|
-
ctx.
|
|
575
|
+
ctx.report(node, "a key written as a computed name; add the 'idKey' brand by hand");
|
|
699
576
|
return;
|
|
700
577
|
}
|
|
701
578
|
if (names.length === 1 && CONVENTIONAL_ID_NAMES.has(names[0])) {
|
|
702
579
|
return;
|
|
703
580
|
}
|
|
704
|
-
const indent = ' '.repeat(node.getSourceFile().getLineAndCharacterOfPosition(ids[0].getStart()).character);
|
|
705
|
-
const brand = names.map(quoted).join(' | ');
|
|
706
581
|
const start = node.members.pos;
|
|
707
|
-
ctx.edits.push({ start, end: start, text: `\n${
|
|
582
|
+
ctx.edits.push({ start, end: start, text: `\n${indentOf(ids[0])}[idKey]?: ${names.map(quoted).join(' | ')};` });
|
|
708
583
|
ctx.imports.set('idKey', 'the brand(s)');
|
|
709
584
|
}
|
|
710
585
|
/**
|
|
711
|
-
*
|
|
712
|
-
*
|
|
586
|
+
* Everything the standard spec needs written onto one decorated property, and the options renamed since.
|
|
587
|
+
* Options it cannot read may still hold `virtual`, so they are reported where they mention it.
|
|
713
588
|
*/
|
|
714
|
-
function addImports(source, rewritten, ctx) {
|
|
715
|
-
const imported = new Set(uqlImports(source).map(importedName));
|
|
716
|
-
const missing = [...ctx.imports].filter(([name]) => !imported.has(name));
|
|
717
|
-
if (!missing.length) {
|
|
718
|
-
return;
|
|
719
|
-
}
|
|
720
|
-
const anchor = uqlImportDeclarations(source).find(({ declaration }) => !rewritten.has(declaration))?.elements[0];
|
|
721
|
-
if (!anchor) {
|
|
722
|
-
for (const [name, what] of missing) {
|
|
723
|
-
ctx.unresolved.push(`${source.fileName}: import '${name}' from 'uql-orm' for ${what} written here`);
|
|
724
|
-
}
|
|
725
|
-
return;
|
|
726
|
-
}
|
|
727
|
-
ctx.edits.push(inserted(anchor, missing.map(([name]) => `${name}, `).join('')));
|
|
728
|
-
}
|
|
729
|
-
/** Everything the standard spec needs written onto one decorated property, and the options renamed since. */
|
|
730
589
|
function rewriteProperty(node, ctx) {
|
|
731
|
-
const owner =
|
|
590
|
+
const owner = ownerOf(node.parent);
|
|
732
591
|
const decorators = decoratorsOf(node);
|
|
733
592
|
for (const decorator of decorators) {
|
|
734
|
-
const name = decoratorName(decorator);
|
|
735
|
-
if (!name) {
|
|
736
|
-
continue;
|
|
737
|
-
}
|
|
593
|
+
const name = decoratorName(decorator) ?? '';
|
|
738
594
|
const options = decoratorOptions(decorator);
|
|
739
595
|
if (FIELD_DECORATORS.has(name)) {
|
|
740
596
|
if (name === 'Field') {
|
|
741
597
|
admitNull(options, node, ctx);
|
|
742
598
|
}
|
|
743
599
|
addFieldType(decorator, node, ctx);
|
|
744
|
-
// Options it cannot read may still hold `virtual`, so they are reported where they mention it.
|
|
745
600
|
if (options.kind === 'opaque' && /\bvirtual\b/.test(decorator.getText())) {
|
|
746
|
-
ctx.
|
|
601
|
+
ctx.report(node, options.reason);
|
|
747
602
|
}
|
|
748
603
|
renameSqlOption(options, 'virtual', 'computed', node, owner, ctx);
|
|
749
604
|
}
|
|
750
605
|
if (RELATION_DECORATORS.has(name)) {
|
|
751
606
|
addRelationEntity(decorator, node, ctx);
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
if (TO_ONE_DECORATORS.has(name)) {
|
|
607
|
+
if (options.kind === 'literal') {
|
|
608
|
+
const target = relationTargetFor(ctx.checker.getTypeAtLocation(node), ctx.checker);
|
|
609
|
+
rewriteRelationOptions(options.node, owner.param, ctx, paramFor(target));
|
|
610
|
+
}
|
|
611
|
+
if (options.kind === 'literal' && TO_ONE_DECORATORS.has(name)) {
|
|
757
612
|
addForeignKeyReference(options.node, propertyKey(node.name), owner, ctx, node);
|
|
758
613
|
}
|
|
759
614
|
}
|
|
@@ -764,34 +619,26 @@ function rewriteProperty(node, ctx) {
|
|
|
764
619
|
unwrapRelationAlias(node, ctx);
|
|
765
620
|
}
|
|
766
621
|
/**
|
|
767
|
-
* A column holds `null` unless `nullable: false` says otherwise,
|
|
768
|
-
*
|
|
769
|
-
* which
|
|
622
|
+
* A column holds `null` unless `nullable: false` says otherwise, so the property says so too. A key is NOT NULL
|
|
623
|
+
* everywhere and a relation aggregate types its own, so neither is touched; nor are options it cannot read,
|
|
624
|
+
* which may state `nullable` and are reported by {@link addFieldType} already.
|
|
770
625
|
*/
|
|
771
626
|
function admitNull(options, node, ctx) {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
return;
|
|
776
|
-
}
|
|
777
|
-
if (declaresNotNull(options) ||
|
|
627
|
+
if (options.kind === 'opaque' ||
|
|
628
|
+
!ctx.strictNullChecks ||
|
|
629
|
+
declaresNotNull(options) ||
|
|
778
630
|
findProperty(options, 'isId') ||
|
|
779
631
|
(findProperty(options, 'computed') && !findProperty(options, 'type'))) {
|
|
780
632
|
return;
|
|
781
633
|
}
|
|
782
634
|
if (!node.type) {
|
|
783
|
-
ctx.
|
|
784
|
-
return;
|
|
635
|
+
ctx.note(node, `declare '${propertyKey(node.name)}' as a type that admits 'null', which the column holds`);
|
|
785
636
|
}
|
|
786
|
-
if (admitsNull(node.type)) {
|
|
787
|
-
|
|
637
|
+
else if (!admitsNull(node.type)) {
|
|
638
|
+
ctx.edits.push(appended(node.type, ' | null'));
|
|
788
639
|
}
|
|
789
|
-
ctx.edits.push(appended(node.type, ' | null'));
|
|
790
640
|
}
|
|
791
|
-
/**
|
|
792
|
-
* The same null rule where `defineEntity` names the field: the property is on the class the call takes,
|
|
793
|
-
* so the declaration is found there rather than under a decorator.
|
|
794
|
-
*/
|
|
641
|
+
/** The same null rule where `defineEntity` names the field, on the property of the class the call takes. */
|
|
795
642
|
function admitNullOnDefined(field, owner, ctx) {
|
|
796
643
|
const key = propertyKey(field.name);
|
|
797
644
|
const declaration = key
|
|
@@ -824,120 +671,23 @@ function reportRemovedDecorators(node, ctx) {
|
|
|
824
671
|
const name = decoratorName(decorator);
|
|
825
672
|
const advice = name && REMOVED_DECORATORS.get(name);
|
|
826
673
|
if (advice) {
|
|
827
|
-
ctx.
|
|
674
|
+
ctx.report(decorator, `'@${name}()' was removed; ${advice}`);
|
|
828
675
|
}
|
|
829
676
|
}
|
|
830
677
|
}
|
|
831
|
-
/** Names an export that no longer exists, where it is imported from the package or one of its entries. */
|
|
832
|
-
function reportRemovedExports(source, ctx) {
|
|
833
|
-
for (const element of uqlImports(source, true)) {
|
|
834
|
-
const name = importedName(element);
|
|
835
|
-
const advice = REMOVED_EXPORTS.get(name);
|
|
836
|
-
if (advice) {
|
|
837
|
-
ctx.unresolved.push(`${ctx.describe(element)}: '${name}' was removed; ${advice}`);
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
/**
|
|
842
|
-
* Rewrites each import naming a {@link RENAMED_EXPORTS} export, renaming every use of it: a name moving
|
|
843
|
-
* entries gets an import from its new one, and a name the file already imports is dropped, as is a `dead`
|
|
844
|
-
* one. Returns the rewritten imports, which nothing else may edit.
|
|
845
|
-
*/
|
|
846
|
-
function renameExports(source, dead, ctx) {
|
|
847
|
-
const imports = uqlImportDeclarations(source, true);
|
|
848
|
-
const kept = imports.flatMap(({ entry, elements }) => elements.filter((element) => !renamedTo(element, entry)));
|
|
849
|
-
const bound = new Set(kept.map((element) => element.name.text));
|
|
850
|
-
const rewritten = new Set();
|
|
851
|
-
for (const { declaration, entry, elements } of imports) {
|
|
852
|
-
if (!elements.some((element) => renamedTo(element, entry))) {
|
|
853
|
-
continue;
|
|
854
|
-
}
|
|
855
|
-
const byEntry = new Map([[entry, []]]);
|
|
856
|
-
for (const element of elements.filter(({ name }) => !dead.has(name.text))) {
|
|
857
|
-
const rename = renamedTo(element, entry);
|
|
858
|
-
const local = element.propertyName || !rename ? element.name.text : rename.to;
|
|
859
|
-
if (rename && !element.propertyName) {
|
|
860
|
-
ctx.edits.push(...usesOf(source, element.name, ctx.checker).map((use) => replaced(use, rename.to)));
|
|
861
|
-
}
|
|
862
|
-
if (rename && bound.has(local)) {
|
|
863
|
-
continue;
|
|
864
|
-
}
|
|
865
|
-
bound.add(local);
|
|
866
|
-
const target = rename?.from ?? entry;
|
|
867
|
-
const text = rename
|
|
868
|
-
? `${element.isTypeOnly ? 'type ' : ''}${rename.to}${element.propertyName ? ` as ${local}` : ''}`
|
|
869
|
-
: element.getText();
|
|
870
|
-
byEntry.set(target, [...(byEntry.get(target) ?? []), text]);
|
|
871
|
-
}
|
|
872
|
-
const typeOnly = declaration.importClause?.phaseModifier === ts.SyntaxKind.TypeKeyword;
|
|
873
|
-
const keyword = typeOnly ? 'import type' : 'import';
|
|
874
|
-
const quote = declaration.moduleSpecifier.getText()[0];
|
|
875
|
-
const statements = [...byEntry]
|
|
876
|
-
.filter(([, names]) => names.length)
|
|
877
|
-
.map(([from, names]) => `${keyword} { ${names.join(', ')} } from ${quote}${from}${quote};`);
|
|
878
|
-
ctx.edits.push(statements.length ? replaced(declaration, statements.join('\n')) : removeStatement(declaration));
|
|
879
|
-
rewritten.add(declaration);
|
|
880
|
-
}
|
|
881
|
-
return rewritten;
|
|
882
|
-
}
|
|
883
|
-
/** The rename `element` still needs, none where it already names the export at its entry. */
|
|
884
|
-
function renamedTo(element, entry) {
|
|
885
|
-
const name = importedName(element);
|
|
886
|
-
const rename = RENAMED_EXPORTS.get(name);
|
|
887
|
-
return rename?.to === name && rename.from === entry ? undefined : rename;
|
|
888
|
-
}
|
|
889
|
-
/** Every identifier in the file bound to the same symbol as `name`, besides `name` itself. */
|
|
890
|
-
function usesOf(source, name, checker) {
|
|
891
|
-
const symbol = checker.getSymbolAtLocation(name);
|
|
892
|
-
const uses = [];
|
|
893
|
-
const visit = (node) => {
|
|
894
|
-
if (ts.isIdentifier(node) && node !== name && node.text === name.text) {
|
|
895
|
-
if (symbol && checker.getSymbolAtLocation(node) === symbol) {
|
|
896
|
-
uses.push(node);
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
ts.forEachChild(node, visit);
|
|
900
|
-
};
|
|
901
|
-
visit(source);
|
|
902
|
-
return uses;
|
|
903
|
-
}
|
|
904
|
-
/**
|
|
905
|
-
* The file's named imports from `uql-orm`. `entries` adds the driver entries (`uql-orm/postgres`, ...),
|
|
906
|
-
* for what is read by name only: `idKey` written into one would land on an entry that may not export it.
|
|
907
|
-
*/
|
|
908
|
-
function uqlImportDeclarations(source, entries = false) {
|
|
909
|
-
return source.statements.flatMap((declaration) => {
|
|
910
|
-
if (!ts.isImportDeclaration(declaration) || !ts.isStringLiteral(declaration.moduleSpecifier)) {
|
|
911
|
-
return [];
|
|
912
|
-
}
|
|
913
|
-
const entry = declaration.moduleSpecifier.text;
|
|
914
|
-
if (entry !== 'uql-orm' && !(entries && entry.startsWith('uql-orm/'))) {
|
|
915
|
-
return [];
|
|
916
|
-
}
|
|
917
|
-
const bindings = declaration.importClause?.namedBindings;
|
|
918
|
-
return bindings && ts.isNamedImports(bindings) ? [{ declaration, entry, elements: bindings.elements }] : [];
|
|
919
|
-
});
|
|
920
|
-
}
|
|
921
|
-
function uqlImports(source, entries = false) {
|
|
922
|
-
return uqlImportDeclarations(source, entries).flatMap(({ elements }) => elements);
|
|
923
|
-
}
|
|
924
|
-
/** The name an import specifier brings in, which is the original one where it was renamed. */
|
|
925
|
-
function importedName(element) {
|
|
926
|
-
return (element.propertyName ?? element.name).text;
|
|
927
|
-
}
|
|
928
678
|
/**
|
|
929
679
|
* Rewrites `raw('sql')` into the tagged template, and a second alias argument into `.as()`, the callback
|
|
930
680
|
* form's included. A computed string is left alone: a template cannot be built from a value not known here.
|
|
931
681
|
*/
|
|
932
|
-
function rewriteRawCall(node, ctx
|
|
933
|
-
if (!ts.isCallExpression(node) ||
|
|
682
|
+
function rewriteRawCall(node, ctx) {
|
|
683
|
+
if (!ts.isCallExpression(node) || identifierText(node.expression) !== 'raw') {
|
|
934
684
|
return;
|
|
935
685
|
}
|
|
936
686
|
const [expression, alias] = node.arguments;
|
|
937
687
|
if (!expression || node.arguments.length > 2) {
|
|
938
688
|
return;
|
|
939
689
|
}
|
|
940
|
-
const suffix = alias ? `.as(${alias.getText(
|
|
690
|
+
const suffix = alias ? `.as(${alias.getText()})` : '';
|
|
941
691
|
if (ts.isStringLiteral(expression)) {
|
|
942
692
|
ctx.edits.push(replaced(node, `${rawTag(expression.text)}${suffix}`));
|
|
943
693
|
}
|
|
@@ -946,169 +696,23 @@ function rewriteRawCall(node, ctx, source) {
|
|
|
946
696
|
}
|
|
947
697
|
}
|
|
948
698
|
/**
|
|
949
|
-
* Rewrites
|
|
950
|
-
*
|
|
951
|
-
* other use is reported. Returns `col` as a dead import once no use is left.
|
|
699
|
+
* Rewrites one source file for the standard decorator spec. The builder's and querier's methods are known by
|
|
700
|
+
* name, so they are read only in a file importing uql-orm, and `expr` only where it is uql's.
|
|
952
701
|
*/
|
|
953
|
-
function
|
|
954
|
-
const
|
|
955
|
-
|
|
956
|
-
return [];
|
|
957
|
-
}
|
|
958
|
-
const computed = new Map();
|
|
959
|
-
const unread = [];
|
|
960
|
-
for (const use of usesOf(source, element.name, ctx.checker)) {
|
|
961
|
-
const call = ts.isCallExpression(use.parent) && use.parent.expression === use ? use.parent : undefined;
|
|
962
|
-
const sql = call && ts.findAncestor(call, isRaw);
|
|
963
|
-
const option = sql?.parent;
|
|
964
|
-
const owner = option && ts.isPropertyAssignment(option) && isComputedOption(option) ? computedOwner(option) : undefined;
|
|
965
|
-
const edit = call && !owner ? statementRefEdit(call, ctx) : undefined;
|
|
966
|
-
if (call && sql && ts.isTaggedTemplateExpression(sql) && owner) {
|
|
967
|
-
computed.set(sql, { owner, calls: [...(computed.get(sql)?.calls ?? []), call] });
|
|
968
|
-
}
|
|
969
|
-
else if (edit) {
|
|
970
|
-
ctx.edits.push(edit);
|
|
971
|
-
ctx.imports.set('refs', 'the refs(...)');
|
|
972
|
-
}
|
|
973
|
-
else {
|
|
974
|
-
unread.push(use);
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
for (const [sql, { owner, calls }] of computed) {
|
|
978
|
-
const edits = computedRefEdits(sql, calls, owner, ctx);
|
|
979
|
-
if (edits) {
|
|
980
|
-
ctx.edits.push(...edits);
|
|
981
|
-
}
|
|
982
|
-
else {
|
|
983
|
-
unread.push(...calls);
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
for (const node of unread.sort((a, b) => a.getStart() - b.getStart())) {
|
|
987
|
-
ctx.unresolved.push(`${ctx.describe(node)}: 'col' was removed; read the column off refs(Entity), which the codemod could not tell here`);
|
|
988
|
-
}
|
|
989
|
-
return unread.length ? [] : ['col'];
|
|
990
|
-
}
|
|
991
|
-
function isComputedOption(option) {
|
|
992
|
-
const key = propertyKey(option.name);
|
|
993
|
-
return key === 'computed' || key === 'virtual';
|
|
994
|
-
}
|
|
995
|
-
/** The entity a field's `computed` SQL is declared on: the decorated property's class, or `defineEntity`'s first argument. */
|
|
996
|
-
function computedOwner(option) {
|
|
997
|
-
const holder = ts.findAncestor(option, (node) => ts.isPropertyDeclaration(node) ||
|
|
998
|
-
(ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'defineEntity'));
|
|
999
|
-
if (holder && ts.isPropertyDeclaration(holder)) {
|
|
1000
|
-
return { param: paramFor(holder.parent.name?.text), entity: holder.parent };
|
|
1001
|
-
}
|
|
1002
|
-
const [entity] = holder?.arguments ?? [];
|
|
1003
|
-
return entity && { param: paramFor(ts.isIdentifier(entity) ? entity.text : undefined), entity };
|
|
1004
|
-
}
|
|
1005
|
-
/** `(product) => ` before a `computed` raw, and `product.x` for each `col('x')` in it, where every one names a member. */
|
|
1006
|
-
function computedRefEdits(sql, calls, owner, ctx) {
|
|
1007
|
-
const members = memberNames(owner.entity, ctx.checker);
|
|
1008
|
-
const names = calls.map((call) => columnMember(call, members));
|
|
1009
|
-
if (mentions(sql, owner.param) || !names.every((name) => name !== undefined)) {
|
|
1010
|
-
return undefined;
|
|
1011
|
-
}
|
|
1012
|
-
return [
|
|
1013
|
-
inserted(sql, `(${owner.param}) => `),
|
|
1014
|
-
...calls.map((call, at) => replaced(call, memberAccess(owner.param, names[at]))),
|
|
1015
|
-
];
|
|
1016
|
-
}
|
|
1017
|
-
/** `refs(Item).x` for a `col('x')` in a statement, where the entity its SQL renders against can be told. */
|
|
1018
|
-
function statementRefEdit(call, ctx) {
|
|
1019
|
-
const entity = statementEntity(call, ctx.checker);
|
|
1020
|
-
const name = entity && columnMember(call, propertyNames(entity.type));
|
|
1021
|
-
return entity && name !== undefined ? replaced(call, memberAccess(`refs(${entity.name})`, name)) : undefined;
|
|
1022
|
-
}
|
|
1023
|
-
/** The member a `col('x')` names, where its one argument is a literal naming one of `members`. */
|
|
1024
|
-
function columnMember(call, members) {
|
|
1025
|
-
const [name] = call.arguments;
|
|
1026
|
-
return call.arguments.length === 1 && ts.isStringLiteralLike(name) && members.has(name.text) ? name.text : undefined;
|
|
1027
|
-
}
|
|
1028
|
-
/** Whether `node` reads a binding named `name`, which a callback parameter of that name would shadow. */
|
|
1029
|
-
function mentions(node, name) {
|
|
1030
|
-
if (ts.isIdentifier(node)) {
|
|
1031
|
-
return node.text === name && !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node);
|
|
1032
|
-
}
|
|
1033
|
-
return ts.forEachChild(node, (child) => mentions(child, name) || undefined) ?? false;
|
|
1034
|
-
}
|
|
1035
|
-
/**
|
|
1036
|
-
* The entity a statement's SQL renders against at `node`: the one its query names, or a relation a `$populate`
|
|
1037
|
-
* reads there. Nothing where a function or a relation filter in between opens a scope of its own.
|
|
1038
|
-
*/
|
|
1039
|
-
function statementEntity(node, checker) {
|
|
1040
|
-
const path = [];
|
|
1041
|
-
for (let at = node; at.parent; at = at.parent) {
|
|
1042
|
-
const { parent } = at;
|
|
1043
|
-
if (ts.isFunctionLike(parent)) {
|
|
1044
|
-
return undefined;
|
|
1045
|
-
}
|
|
1046
|
-
if (ts.isPropertyAssignment(parent)) {
|
|
1047
|
-
path.unshift(propertyKey(parent.name) ?? '');
|
|
1048
|
-
}
|
|
1049
|
-
const queried = ts.isCallExpression(parent) && parent.arguments.some((argument) => argument === at);
|
|
1050
|
-
const entity = queried ? queriedEntity(parent, checker) : undefined;
|
|
1051
|
-
if (entity) {
|
|
1052
|
-
return entityAlong(entity, path, node, checker);
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
return undefined;
|
|
1056
|
-
}
|
|
1057
|
-
/** The entity a call queries: its first argument's class, or the `$entity` its query names. */
|
|
1058
|
-
function queriedEntity(call, checker) {
|
|
1059
|
-
const [first] = call.arguments;
|
|
1060
|
-
const entity = first && ts.isObjectLiteralExpression(first) ? propertyValue(first, '$entity') : first;
|
|
1061
|
-
const instance = entity && checker.getTypeAtLocation(entity).getConstructSignatures()[0]?.getReturnType();
|
|
1062
|
-
return entity && ts.isIdentifier(entity) && instance ? { name: entity.text, type: instance } : undefined;
|
|
1063
|
-
}
|
|
1064
|
-
/** `entity` followed along `path`: into each relation a `$populate` names, and nowhere a relation filter opens. */
|
|
1065
|
-
function entityAlong(entity, path, node, checker) {
|
|
1066
|
-
let current = entity;
|
|
1067
|
-
for (const [at, key] of path.entries()) {
|
|
1068
|
-
const property = current.type.getProperty(key);
|
|
1069
|
-
const related = property && relatedClass(property, node, checker);
|
|
1070
|
-
if (!related) {
|
|
1071
|
-
continue;
|
|
1072
|
-
}
|
|
1073
|
-
const inScope = checker.getSymbolsInScope(node, ts.SymbolFlags.Class).some(({ name }) => name === related.name);
|
|
1074
|
-
if (path[at - 1] !== '$populate' || !inScope) {
|
|
1075
|
-
return undefined;
|
|
1076
|
-
}
|
|
1077
|
-
current = { name: related.name, type: checker.getDeclaredTypeOfSymbol(related) };
|
|
1078
|
-
}
|
|
1079
|
-
return current;
|
|
1080
|
-
}
|
|
1081
|
-
/** The class a relation property points at, one or many, or nothing where the property holds no class. */
|
|
1082
|
-
function relatedClass(property, node, checker) {
|
|
1083
|
-
const type = checker.getNonNullableType(checker.getTypeOfSymbolAtLocation(property, node));
|
|
1084
|
-
const symbol = (checker.getIndexTypeOfType(type, ts.IndexKind.Number) ?? type).getSymbol();
|
|
1085
|
-
return symbol && symbol.flags & ts.SymbolFlags.Class ? symbol : undefined;
|
|
1086
|
-
}
|
|
1087
|
-
/** Rewrites one source file for the standard decorator spec. */
|
|
1088
|
-
export function transformFile(source, checker) {
|
|
1089
|
-
const ctx = {
|
|
1090
|
-
checker,
|
|
1091
|
-
edits: [],
|
|
1092
|
-
unresolved: [],
|
|
1093
|
-
notes: [],
|
|
1094
|
-
imports: new Map(),
|
|
1095
|
-
rewritten: { Relation: 0, expr: 0 },
|
|
1096
|
-
describe: (node) => `${source.fileName}:${lineOf(node) + 1}`,
|
|
1097
|
-
};
|
|
1098
|
-
const lineOf = (node) => source.getLineAndCharacterOfPosition(node.getStart()).line;
|
|
1099
|
-
const rewritesRaw = uqlImports(source).some((element) => importedName(element) === 'raw');
|
|
1100
|
-
// The builder's methods are known by name alone, so they are read only in a file that imports uql-orm.
|
|
702
|
+
export function transformFile(source, checker, options = { strictNullChecks: true }) {
|
|
703
|
+
const ctx = createContext(source, checker, options.strictNullChecks);
|
|
704
|
+
const rewritesRaw = uqlImport(source, 'raw') !== undefined;
|
|
1101
705
|
const importsUql = uqlImportDeclarations(source, true).length > 0;
|
|
1102
|
-
const importsExpr =
|
|
706
|
+
const importsExpr = uqlImport(source, 'expr', true) !== undefined;
|
|
1103
707
|
const visit = (node) => {
|
|
1104
708
|
reportRemovedDecorators(node, ctx);
|
|
1105
709
|
if (rewritesRaw) {
|
|
1106
|
-
rewriteRawCall(node, ctx
|
|
710
|
+
rewriteRawCall(node, ctx);
|
|
1107
711
|
}
|
|
1108
712
|
if (ts.isPropertyDeclaration(node)) {
|
|
1109
713
|
rewriteProperty(node, ctx);
|
|
1110
714
|
}
|
|
1111
|
-
if (ts.
|
|
715
|
+
if (ts.isClassLike(node)) {
|
|
1112
716
|
brandIdKey(node, ctx);
|
|
1113
717
|
rewriteClassDecorators(node, ctx);
|
|
1114
718
|
}
|
|
@@ -1117,6 +721,7 @@ export function transformFile(source, checker) {
|
|
|
1117
721
|
rewriteUpsertCall(node, ctx);
|
|
1118
722
|
if (importsUql) {
|
|
1119
723
|
rewriteBuilderCall(node, ctx);
|
|
724
|
+
rewriteMethodCall(node, ctx);
|
|
1120
725
|
}
|
|
1121
726
|
if (importsExpr) {
|
|
1122
727
|
rewriteExprCall(node, ctx);
|
|
@@ -1125,14 +730,17 @@ export function transformFile(source, checker) {
|
|
|
1125
730
|
if (ts.isPropertyAssignment(node)) {
|
|
1126
731
|
rewriteStatementKeys(node, ctx);
|
|
1127
732
|
}
|
|
733
|
+
if (importsUql && ts.isVariableDeclaration(node)) {
|
|
734
|
+
rewriteUpsertResult(node, ctx);
|
|
735
|
+
}
|
|
1128
736
|
ts.forEachChild(node, visit);
|
|
1129
737
|
};
|
|
1130
738
|
visit(source);
|
|
1131
|
-
const dead = new Set([...deadImportNames(
|
|
1132
|
-
const rewritten = renameExports(
|
|
1133
|
-
reportRemovedExports(
|
|
1134
|
-
dropDeadImports(
|
|
1135
|
-
addImports(
|
|
739
|
+
const dead = new Set([...deadImportNames(ctx), ...rewriteColumnRefs(ctx)]);
|
|
740
|
+
const rewritten = renameExports(dead, ctx);
|
|
741
|
+
reportRemovedExports(ctx);
|
|
742
|
+
dropDeadImports(dead, rewritten, ctx);
|
|
743
|
+
addImports(rewritten, ctx);
|
|
1136
744
|
return {
|
|
1137
745
|
fileName: source.fileName,
|
|
1138
746
|
text: applyEdits(source.getFullText(), ctx.edits),
|