uql-codemod 0.47.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/dist/cli.d.ts +3 -6
- package/dist/cli.js +3 -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 -2
- package/dist/entitySql.js +10 -6
- 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/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 +4 -1
- package/dist/transform.js +153 -691
- 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,145 +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/mariadb`)'],
|
|
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
|
-
...['JsonMigrationStorage', 'DatabaseMigrationStorage', 'MongoMigrationStorage'].map((name) => [name, 'the migrator keeps its journal in the database itself, named by `tableName`']),
|
|
64
|
-
]);
|
|
65
|
-
/** What `uql-orm/util` exported that the root still does. */
|
|
66
|
-
const UTIL_ROOT_EXPORTS = [
|
|
67
|
-
'currentDate',
|
|
68
|
-
'currentTime',
|
|
69
|
-
'currentTimestamp',
|
|
70
|
-
'raw',
|
|
71
|
-
'refs',
|
|
72
|
-
'uuid',
|
|
73
|
-
'uuidv7',
|
|
74
|
-
'deleteFrom',
|
|
75
|
-
'insertInto',
|
|
76
|
-
'refuse',
|
|
77
|
-
'updateTable',
|
|
78
|
-
'upsertInto',
|
|
79
|
-
'withDeleted',
|
|
80
|
-
'HookContext',
|
|
81
|
-
'DefaultLogger',
|
|
82
|
-
];
|
|
83
|
-
/**
|
|
84
|
-
* Exports renamed or moved and nothing else, so the import and every use follow, the import moving to `from`
|
|
85
|
-
* where the name lives in another entry. Keyed by name, or by `entry#name` where the name stays elsewhere.
|
|
86
|
-
*/
|
|
87
|
-
const RENAMED_EXPORTS = new Map([
|
|
88
|
-
['QueryWhereMap', { to: 'QueryWhere' }],
|
|
89
|
-
['RelationKeyMap', { to: 'KeyMap' }],
|
|
90
|
-
['FilterCondition', { to: 'FilterWhere' }],
|
|
91
|
-
['PgDialect', { to: 'PostgresDialect', from: 'uql-orm/postgres' }],
|
|
92
|
-
['NeonDialect', { to: 'PostgresDialect', from: 'uql-orm/postgres' }],
|
|
93
|
-
['PgliteDialect', { to: 'PostgresDialect', from: 'uql-orm/postgres' }],
|
|
94
|
-
['CrdbQuerier', { to: 'PgQuerier', from: 'uql-orm/postgres' }],
|
|
95
|
-
['NeonQuerier', { to: 'PgQuerier', from: 'uql-orm/postgres' }],
|
|
96
|
-
['MySql2Dialect', { to: 'MySqlDialect', from: 'uql-orm/mysql' }],
|
|
97
|
-
['MongodbNativeDialect', { to: 'MongoDialect', from: 'uql-orm/mongodb' }],
|
|
98
|
-
['LibsqlQuerier', { to: 'HranaQuerier', from: 'uql-orm/sqlite' }],
|
|
99
|
-
['TursoQuerier', { to: 'HranaQuerier', from: 'uql-orm/sqlite' }],
|
|
100
|
-
['TursoLocalQuerier', { to: 'SqliteQuerier', from: 'uql-orm/sqlite' }],
|
|
101
|
-
['TursoDatabase', { to: 'SqliteDatabase', from: 'uql-orm/sqlite' }],
|
|
102
|
-
['SqlMigrationModuleOptions', { to: 'MigrationModuleOptions' }],
|
|
103
|
-
['buildSqlQuerierMigrationModule', { to: 'buildMigrationModule' }],
|
|
104
|
-
['QueryDialect', { to: 'SqlQueryDialect' }],
|
|
105
|
-
['EngineFeatures', { to: 'DialectFeatures' }],
|
|
106
|
-
['KnownMigratorDialect', { to: 'DialectName', from: 'uql-orm' }],
|
|
107
|
-
['D1Preparer', { to: 'D1Queryable' }],
|
|
108
|
-
['D1Database', { to: 'D1Queryable', from: 'uql-orm/d1' }],
|
|
109
|
-
['MongoQuerier', { to: 'MongoQuerier', from: 'uql-orm/mongodb' }],
|
|
110
|
-
['isMongoQuerier', { to: 'isMongoQuerier', from: 'uql-orm/mongodb' }],
|
|
111
|
-
['MongoSchemaIntrospector', { to: 'MongoSchemaIntrospector', from: 'uql-orm/mongodb' }],
|
|
112
|
-
['Sqlite3QuerierPool', { to: 'SqliteQuerierPool' }],
|
|
113
|
-
['Sqlite3PoolOptions', { to: 'SqlitePoolOptions' }],
|
|
114
|
-
['IMigrationBuilder', { to: 'MigrationBuilder' }],
|
|
115
|
-
['ITableBuilder', { to: 'TableBuilder' }],
|
|
116
|
-
['IAlterTableBuilder', { to: 'AlterTableBuilder' }],
|
|
117
|
-
['IColumnBuilder', { to: 'ColumnBuilder' }],
|
|
118
|
-
['IColumnFactory', { to: 'ColumnFactory' }],
|
|
119
|
-
['IForeignKeyBuilder', { to: 'ForeignKeyBuilder' }],
|
|
120
|
-
['ITableForeignKeyBuilder', { to: 'TableForeignKeyBuilder' }],
|
|
121
|
-
['UqlLockUsageError', { to: 'UqlUsageError' }],
|
|
122
|
-
['uql-orm/http#Hook', { to: 'RequestHook' }],
|
|
123
|
-
['uql-orm/http#HookContext', { to: 'RequestHookContext' }],
|
|
124
|
-
...UTIL_ROOT_EXPORTS.map((name) => [`uql-orm/util#${name}`, { to: name, from: 'uql-orm' }]),
|
|
125
|
-
]);
|
|
126
|
-
/** What the root exported that is internal now, and what to write instead. */
|
|
127
|
-
const INTERNAL_EXPORTS = new Map([
|
|
128
|
-
...['AbstractDialect', 'AbstractSqlDialect', 'AbstractQuerier', 'AbstractQuerierPool'].map((name) => [name, "extend a driver's dialect or pool from its entry instead"]),
|
|
129
|
-
...['AbstractSqlQuerier', 'AbstractSqlQuerierPool'].map((name) => [name, "extend a driver's querier or pool from its entry instead"]),
|
|
130
|
-
['SqlQueryContext', 'make one with `dialect.createContext()`, typed `QueryContext`'],
|
|
131
|
-
['dialectOptionsFrom', "pass the options to the driver's dialect"],
|
|
132
|
-
...['fieldOf', 'relationOf', 'soleIdOf', 'assertSoleId'].map((name) => [name, 'read the field off `getMeta(Entity).fields`']),
|
|
133
|
-
['namesKey', 'read the keys off `getMeta(Entity).ids`'],
|
|
134
|
-
['getEntities', 'list your entities in the config'],
|
|
135
|
-
]);
|
|
136
|
-
/** Entries renamed, or dropped for repeating the root, each to the one now exporting what it did. */
|
|
137
|
-
const MOVED_ENTRIES = new Map([
|
|
138
|
-
['uql-orm/maria', 'uql-orm/mariadb'],
|
|
139
|
-
['uql-orm/mongo', 'uql-orm/mongodb'],
|
|
140
|
-
['uql-orm/bunSql', 'uql-orm/bun-sql'],
|
|
141
|
-
['uql-orm/dialect', 'uql-orm'],
|
|
142
|
-
['uql-orm/entity', 'uql-orm'],
|
|
143
|
-
['uql-orm/namingStrategy', 'uql-orm'],
|
|
144
|
-
['uql-orm/querier', 'uql-orm'],
|
|
145
|
-
['uql-orm/type', 'uql-orm'],
|
|
146
|
-
]);
|
|
147
|
-
/** A node's decorators, or none where it is a kind that cannot carry them. */
|
|
148
|
-
function decoratorsOf(node) {
|
|
149
|
-
return ts.canHaveDecorators(node) ? (ts.getDecorators(node) ?? []) : [];
|
|
150
|
-
}
|
|
151
|
-
function decoratorName(node) {
|
|
152
|
-
const call = ts.isCallExpression(node.expression) ? node.expression : undefined;
|
|
153
|
-
const target = call?.expression ?? node.expression;
|
|
154
|
-
return ts.isIdentifier(target) ? target.text : undefined;
|
|
155
|
-
}
|
|
26
|
+
/** A decorator's options; `@Field` uncalled is opaque, since adding a call would guess it is the factory. */
|
|
156
27
|
function decoratorOptions(node) {
|
|
157
28
|
if (!ts.isCallExpression(node.expression)) {
|
|
158
|
-
// `@Field` rather than `@Field()`: there is no argument list to write into, and adding one would mean
|
|
159
|
-
// guessing that this is the decorator factory the codemod thinks it is.
|
|
160
29
|
return { kind: 'opaque', reason: 'it is used without being called' };
|
|
161
30
|
}
|
|
162
31
|
const [first] = node.expression.arguments;
|
|
@@ -171,18 +40,9 @@ function optionsOf(value) {
|
|
|
171
40
|
? { kind: 'opaque', reason: 'its options object spreads another' }
|
|
172
41
|
: { kind: 'literal', node: value };
|
|
173
42
|
}
|
|
174
|
-
/**
|
|
175
|
-
* The property `name` in the options, whether its key is plain, quoted or a shorthand.
|
|
176
|
-
*
|
|
177
|
-
* All three forms, because both callers care about the option being *there*: one to leave an option
|
|
178
|
-
* alone that is already stated, the other to rename it. Matching only the plain key inserted a second
|
|
179
|
-
* `type` beside a quoted one, and left a shorthand `virtual` behind while reporting nothing.
|
|
180
|
-
*/
|
|
43
|
+
/** The option `name`, however its key is written: a stated option is left alone, and a renamed one moved. */
|
|
181
44
|
function findProperty(options, name) {
|
|
182
|
-
|
|
183
|
-
return undefined;
|
|
184
|
-
}
|
|
185
|
-
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;
|
|
186
46
|
}
|
|
187
47
|
/**
|
|
188
48
|
* Renames the option `from` to `to`. Only the key is replaced, so the value, its formatting and any comment
|
|
@@ -195,7 +55,7 @@ function renameOption(options, from, to, node, ctx) {
|
|
|
195
55
|
return;
|
|
196
56
|
}
|
|
197
57
|
if (findProperty(options, to)) {
|
|
198
|
-
ctx.
|
|
58
|
+
ctx.report(node, `gives both '${from}' and '${to}'; keep '${to}'.`);
|
|
199
59
|
return;
|
|
200
60
|
}
|
|
201
61
|
ctx.edits.push(replaced(property.name, ts.isShorthandPropertyAssignment(property) ? `${to}: ${from}` : to));
|
|
@@ -213,25 +73,12 @@ function noteHandNamedColumns(sql, owner, ctx) {
|
|
|
213
73
|
if (!sql || !names.length) {
|
|
214
74
|
return;
|
|
215
75
|
}
|
|
216
|
-
ctx.
|
|
217
|
-
`
|
|
218
|
-
}
|
|
219
|
-
/** The names of the members of the entity `node` declares or names. */
|
|
220
|
-
function memberNames(node, checker) {
|
|
221
|
-
return propertyNames(instanceTypeOf(node, checker));
|
|
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>}...\``);
|
|
222
78
|
}
|
|
223
|
-
/**
|
|
224
|
-
function instanceTypeOf(node, checker) {
|
|
225
|
-
const type = checker.getTypeAtLocation(node);
|
|
226
|
-
return type.getConstructSignatures()[0]?.getReturnType() ?? type;
|
|
227
|
-
}
|
|
228
|
-
function propertyNames(type) {
|
|
229
|
-
return new Set(type.getProperties().map(({ name }) => name));
|
|
230
|
-
}
|
|
231
|
-
/** Inserts `option` into the decorator's options object, creating one when the call has no arguments. */
|
|
79
|
+
/** Inserts `option` into the decorator's options object, creating one in an empty argument list. */
|
|
232
80
|
function insertOption(options, option) {
|
|
233
81
|
if (options.kind === 'empty') {
|
|
234
|
-
// `@Field()` -> `@Field({ ... })`: the argument list is empty, so this only ever inserts.
|
|
235
82
|
const { arguments: args } = options.call;
|
|
236
83
|
return { start: args.pos, end: args.end, text: `{ ${option} }` };
|
|
237
84
|
}
|
|
@@ -239,11 +86,8 @@ function insertOption(options, option) {
|
|
|
239
86
|
return first ? inserted(first, `${option}, `) : replaced(options.node, `{ ${option} }`);
|
|
240
87
|
}
|
|
241
88
|
/**
|
|
242
|
-
* Writes one option the decorator can no longer infer at runtime, or records why it could not.
|
|
243
|
-
*
|
|
244
|
-
* Returns the property's type when an edit was written, so a caller can say something about what was
|
|
245
|
-
* just decided. Nothing is written for options the codemod cannot read: replacing them would drop
|
|
246
|
-
* 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.
|
|
247
91
|
*/
|
|
248
92
|
function addInferredOption(decorator, node, ctx, spec) {
|
|
249
93
|
const options = decoratorOptions(decorator);
|
|
@@ -251,34 +95,32 @@ function addInferredOption(decorator, node, ctx, spec) {
|
|
|
251
95
|
return undefined;
|
|
252
96
|
}
|
|
253
97
|
if (options.kind === 'opaque') {
|
|
254
|
-
ctx.
|
|
98
|
+
ctx.report(node, `cannot add '${spec.option}' because ${options.reason}`);
|
|
255
99
|
return undefined;
|
|
256
100
|
}
|
|
257
101
|
const type = ctx.checker.getTypeAtLocation(node);
|
|
258
102
|
const value = spec.infer(type);
|
|
259
103
|
if (!value) {
|
|
260
|
-
ctx.
|
|
104
|
+
ctx.report(node, `cannot infer '${spec.option}' for ${ctx.checker.typeToString(type)}`);
|
|
261
105
|
return undefined;
|
|
262
106
|
}
|
|
263
107
|
ctx.edits.push(insertOption(options, `${spec.option}: ${value}`));
|
|
264
108
|
return type;
|
|
265
109
|
}
|
|
266
|
-
/**
|
|
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
|
+
*/
|
|
267
115
|
function addFieldType(decorator, node, ctx) {
|
|
268
116
|
const type = addInferredOption(decorator, node, ctx, {
|
|
269
117
|
option: 'type',
|
|
270
|
-
// A `references` field deliberately has no `type`: the column resolves from the primary key it
|
|
271
|
-
// points at, which also carries that key's `columnType` and length. A `computed` one is its SQL.
|
|
272
118
|
satisfiedBy: ['type', 'references', 'computed'],
|
|
273
119
|
infer: fieldTypeFor,
|
|
274
120
|
});
|
|
275
|
-
// A branded id type (`type UUID = `${string}-${string}-...``) erased to `String` under reflection, so
|
|
276
|
-
// `String` is what keeps the existing column. It is often not what was wanted, though, and switching
|
|
277
|
-
// to `'uuid'` changes the generated column, so say so rather than decide.
|
|
278
121
|
if (type && isBrandedString(type)) {
|
|
279
|
-
ctx.
|
|
280
|
-
|
|
281
|
-
"'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.");
|
|
282
124
|
}
|
|
283
125
|
}
|
|
284
126
|
/** Writes the `entity` getter relations can no longer infer. */
|
|
@@ -298,10 +140,9 @@ function addRelationEntity(decorator, node, ctx) {
|
|
|
298
140
|
* is declared, or `defineEntity`'s `fields`. A column it cannot see declared is reported instead.
|
|
299
141
|
*/
|
|
300
142
|
function addForeignKeyReference(relation, key, owner, ctx, site) {
|
|
301
|
-
const options = { kind: 'literal', node: relation };
|
|
302
143
|
const cardinality = propertyValue(relation, 'cardinality');
|
|
303
144
|
const toOne = !cardinality || (ts.isStringLiteralLike(cardinality) && TO_ONE_CARDINALITIES.has(cardinality.text));
|
|
304
|
-
const joined = ['mappedBy', 'through', 'references'].some((option) =>
|
|
145
|
+
const joined = ['mappedBy', 'through', 'references'].some((option) => namedProperty(relation, option));
|
|
305
146
|
const last = relation.properties.at(-1);
|
|
306
147
|
if (!key || !last || !toOne || joined) {
|
|
307
148
|
return;
|
|
@@ -309,7 +150,7 @@ function addForeignKeyReference(relation, key, owner, ctx, site) {
|
|
|
309
150
|
const column = `${key}Id`;
|
|
310
151
|
const declaration = columnDeclaration(column, relation, owner, ctx.checker, site);
|
|
311
152
|
if (typeof declaration === 'string') {
|
|
312
|
-
ctx.
|
|
153
|
+
ctx.report(site && ts.isPropertyDeclaration(site) ? site : relation, declaration);
|
|
313
154
|
return;
|
|
314
155
|
}
|
|
315
156
|
if (declaration) {
|
|
@@ -331,7 +172,7 @@ function columnDeclaration(column, relation, owner, checker, site) {
|
|
|
331
172
|
const declared = site ? objectProperties(site).some(({ name }) => propertyKey(name) === column) : member;
|
|
332
173
|
return declared ? undefined : undeclaredColumn(column);
|
|
333
174
|
}
|
|
334
|
-
/** `@Field({ references: () => Target }) <column>?: <key type
|
|
175
|
+
/** `@Field({ references: () => Target }) <column>?: <key type> | null;` ahead of the relation, or why it cannot be written. */
|
|
335
176
|
function foreignKeyColumn(property, column, relation, checker) {
|
|
336
177
|
const targetType = checker.getNonNullableType(checker.getTypeAtLocation(property));
|
|
337
178
|
const target = entityGetterTarget(relation) ?? relationTargetFor(targetType, checker);
|
|
@@ -343,10 +184,12 @@ function foreignKeyColumn(property, column, relation, checker) {
|
|
|
343
184
|
if (!target || !key) {
|
|
344
185
|
return undeclaredColumn(column);
|
|
345
186
|
}
|
|
346
|
-
const indent = ' '.repeat(property.getSourceFile().getLineAndCharacterOfPosition(property.getStart()).character);
|
|
347
187
|
const type = keyTypeSource(key, property, checker);
|
|
348
|
-
|
|
349
|
-
|
|
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);
|
|
350
193
|
}
|
|
351
194
|
/** The key's type as written where that is in scope, the same file, or as the checker spells it out. */
|
|
352
195
|
function keyTypeSource(key, at, checker) {
|
|
@@ -374,7 +217,7 @@ function rewriteValue(value, rewrite, node, advice, ctx) {
|
|
|
374
217
|
ctx.edits.push(...edits);
|
|
375
218
|
}
|
|
376
219
|
else {
|
|
377
|
-
ctx.
|
|
220
|
+
ctx.report(node, `${advice}; this one could not be read`);
|
|
378
221
|
}
|
|
379
222
|
}
|
|
380
223
|
function rewriteKeyList(value, param, node, what, ctx) {
|
|
@@ -385,21 +228,22 @@ function rewriteKeyList(value, param, node, what, ctx) {
|
|
|
385
228
|
* members named by string read off a callback's parameter named after the class, and SQL written as it is taken.
|
|
386
229
|
*/
|
|
387
230
|
function rewriteClassDecorators(node, ctx) {
|
|
388
|
-
const owner =
|
|
231
|
+
const owner = ownerOf(node);
|
|
389
232
|
for (const decorator of decoratorsOf(node)) {
|
|
390
233
|
if (!ts.isCallExpression(decorator.expression)) {
|
|
391
234
|
continue;
|
|
392
235
|
}
|
|
393
236
|
const [first, second] = decorator.expression.arguments;
|
|
394
237
|
const name = decoratorName(decorator);
|
|
238
|
+
const options = objectLiteral(first);
|
|
395
239
|
if (name === 'Index') {
|
|
396
240
|
rewriteIndex(first, second, owner, decorator, "'@Index' columns", ctx);
|
|
397
241
|
}
|
|
398
242
|
else if (name === 'Filter' && second) {
|
|
399
243
|
renameOption(optionsOf(second), 'condition', 'where', decorator, ctx);
|
|
400
244
|
}
|
|
401
|
-
else if (name === 'Entity' &&
|
|
402
|
-
rewriteEntityOptions(
|
|
245
|
+
else if (name === 'Entity' && options) {
|
|
246
|
+
rewriteEntityOptions(options, owner, ctx);
|
|
403
247
|
}
|
|
404
248
|
}
|
|
405
249
|
}
|
|
@@ -409,18 +253,20 @@ function rewriteClassDecorators(node, ctx) {
|
|
|
409
253
|
* Only uql's shapes, a class and a literal options object, are read.
|
|
410
254
|
*/
|
|
411
255
|
function rewriteDefineCall(call, ctx) {
|
|
412
|
-
const name =
|
|
256
|
+
const name = identifierText(call.expression);
|
|
413
257
|
const [entity = call, second, third] = call.arguments;
|
|
414
|
-
const owner =
|
|
415
|
-
|
|
416
|
-
|
|
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);
|
|
417
263
|
}
|
|
418
|
-
else if (name === 'defineIndex' &&
|
|
419
|
-
rewriteIndexOptions(
|
|
264
|
+
else if (name === 'defineIndex' && options) {
|
|
265
|
+
rewriteIndexOptions(options, owner, ctx);
|
|
420
266
|
}
|
|
421
|
-
else if (name === 'defineRelation' &&
|
|
422
|
-
rewriteRelationOptions(
|
|
423
|
-
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);
|
|
424
270
|
}
|
|
425
271
|
else if (name === 'defineFilter' && third) {
|
|
426
272
|
renameOption(optionsOf(third), 'condition', 'where', call, ctx);
|
|
@@ -446,11 +292,10 @@ function rewriteEntityOptions(options, owner, ctx) {
|
|
|
446
292
|
for (const hook of objectProperties(propertyValue(options, 'hooks'))) {
|
|
447
293
|
rewriteKeyList(hook.initializer, owner.param, hook, 'a hook list', ctx);
|
|
448
294
|
}
|
|
449
|
-
const declared = fields && ts.isObjectLiteralExpression(fields) ? fields : undefined;
|
|
450
295
|
for (const relation of objectProperties(propertyValue(options, 'relations'))) {
|
|
451
296
|
if (ts.isObjectLiteralExpression(relation.initializer)) {
|
|
452
297
|
rewriteRelationOptions(relation.initializer, owner.param, ctx);
|
|
453
|
-
addForeignKeyReference(relation.initializer, propertyKey(relation.name), owner, ctx,
|
|
298
|
+
addForeignKeyReference(relation.initializer, propertyKey(relation.name), owner, ctx, objectLiteral(fields));
|
|
454
299
|
}
|
|
455
300
|
}
|
|
456
301
|
}
|
|
@@ -462,7 +307,7 @@ function rewriteIndexOptions(index, owner, ctx) {
|
|
|
462
307
|
* `include` read off the entity's refs, its `where` written as {@link rewriteIndexWhere} writes it, its SQL noted.
|
|
463
308
|
*/
|
|
464
309
|
function rewriteIndex(columns, options, owner, node, what, ctx) {
|
|
465
|
-
const literal =
|
|
310
|
+
const literal = objectLiteral(options);
|
|
466
311
|
rewriteKeyList(columns, owner.param, node, what, ctx);
|
|
467
312
|
rewriteKeyList(literal && propertyValue(literal, 'include'), owner.param, node, "'include'", ctx);
|
|
468
313
|
rewriteIndexWhere(literal, ctx);
|
|
@@ -470,10 +315,7 @@ function rewriteIndex(columns, options, owner, node, what, ctx) {
|
|
|
470
315
|
noteHandNamedColumns(sql, owner, ctx);
|
|
471
316
|
}
|
|
472
317
|
}
|
|
473
|
-
/**
|
|
474
|
-
* A partial-index `where` string as `raw`, on an entity and in the migration builder alike. A string it
|
|
475
|
-
* cannot write as `raw` is reported.
|
|
476
|
-
*/
|
|
318
|
+
/** A partial-index `where` string as `raw`, on an entity and in the migration builder alike, or reported. */
|
|
477
319
|
function rewriteIndexWhere(options, ctx) {
|
|
478
320
|
const where = options && propertyValue(options, 'where');
|
|
479
321
|
if (!where || !isStringTyped(ctx.checker.getTypeAtLocation(where))) {
|
|
@@ -485,82 +327,78 @@ function rewriteIndexWhere(options, ctx) {
|
|
|
485
327
|
ctx.imports.set('raw', 'the raw`...`');
|
|
486
328
|
}
|
|
487
329
|
else {
|
|
488
|
-
ctx.
|
|
330
|
+
ctx.report(where, "write the partial-index 'where' as raw`...` or a predicate; this one could not be read");
|
|
489
331
|
}
|
|
490
332
|
}
|
|
491
333
|
/** Whether every value `type` admits is a string. */
|
|
492
334
|
function isStringTyped(type) {
|
|
493
335
|
return type.isUnion() ? type.types.every(isStringTyped) : Boolean(type.flags & ts.TypeFlags.StringLike);
|
|
494
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
|
+
}
|
|
495
360
|
/**
|
|
496
361
|
* A querier's `run`/`all` handed SQL as a string, which now takes a tagged statement: a literal becomes
|
|
497
|
-
* `run`...``,
|
|
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.
|
|
498
364
|
*/
|
|
499
|
-
function rewriteSqlCall(call, ctx) {
|
|
500
|
-
const callee = call.expression;
|
|
501
|
-
if (!ts.isPropertyAccessExpression(callee) || !SQL_METHODS.has(callee.name.text) || !isUqlMember(callee, ctx)) {
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
365
|
+
function rewriteSqlCall(call, method, ctx) {
|
|
504
366
|
const [sql, ...values] = call.arguments;
|
|
505
367
|
if (!sql || !isStringTyped(ctx.checker.getTypeAtLocation(sql))) {
|
|
506
368
|
return;
|
|
507
369
|
}
|
|
508
|
-
|
|
509
|
-
|
|
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}`));
|
|
510
374
|
return;
|
|
511
375
|
}
|
|
512
|
-
if (
|
|
513
|
-
|
|
514
|
-
ctx.edits.push(replaced(call, `${callee.getText()}${typeArguments}${templateOf(sql.text)}`));
|
|
376
|
+
if (values.length) {
|
|
377
|
+
ctx.report(call, `${method}() takes one raw statement: write the values into it, raw\`... \${value}\`, which binds them`);
|
|
515
378
|
return;
|
|
516
379
|
}
|
|
517
380
|
if (ts.isTemplateExpression(sql)) {
|
|
518
|
-
ctx.
|
|
381
|
+
ctx.note(call, 'the values in this template are spliced into the SQL; write it as raw`...` to bind them');
|
|
519
382
|
}
|
|
520
383
|
ctx.edits.push(replaced(sql, `raw.text(${sql.getText()})`));
|
|
521
384
|
ctx.imports.set('raw', 'the raw.text(...)');
|
|
522
385
|
}
|
|
523
|
-
const SQL_METHODS = new Set(['run', 'all']);
|
|
524
386
|
/** `const { id } = await q.upsertOne(...)`, which resolves to the id now, as the id: other keys are reported. */
|
|
525
387
|
function rewriteUpsertResult(declaration, ctx) {
|
|
526
388
|
const { name, initializer } = declaration;
|
|
527
389
|
const call = initializer && ts.isAwaitExpression(initializer) ? initializer.expression : initializer;
|
|
528
|
-
if (!ts.isObjectBindingPattern(name) ||
|
|
529
|
-
!call ||
|
|
530
|
-
!ts.isCallExpression(call) ||
|
|
531
|
-
!ts.isPropertyAccessExpression(call.expression) ||
|
|
532
|
-
call.expression.name.text !== 'upsertOne' ||
|
|
533
|
-
!isUqlMember(call.expression, ctx)) {
|
|
390
|
+
if (!ts.isObjectBindingPattern(name) || !call || !ts.isCallExpression(call) || uqlMethod(call, ctx) !== 'upsertOne') {
|
|
534
391
|
return;
|
|
535
392
|
}
|
|
536
393
|
const [element] = name.elements;
|
|
537
394
|
const key = element && (element.propertyName ?? element.name);
|
|
538
|
-
if (name.elements.length === 1 && key &&
|
|
395
|
+
if (name.elements.length === 1 && key && identifierText(key) === 'id' && ts.isIdentifier(element.name)) {
|
|
539
396
|
ctx.edits.push(replaced(name, element.name.text));
|
|
540
397
|
}
|
|
541
398
|
else {
|
|
542
|
-
ctx.
|
|
399
|
+
ctx.report(declaration, "upsertOne() resolves to the id; 'created' and 'changes' are gone");
|
|
543
400
|
}
|
|
544
401
|
}
|
|
545
|
-
/** A `Migrator` call whose name or meaning changed: `pending()`/`executed()` are `status()`, and `down()` reverts one. */
|
|
546
|
-
function reportMigratorCall(call, ctx) {
|
|
547
|
-
const callee = call.expression;
|
|
548
|
-
if (!ts.isPropertyAccessExpression(callee) || !isUqlMember(callee, ctx)) {
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
const method = callee.name.text;
|
|
552
|
-
if (method === 'pending' || method === 'executed') {
|
|
553
|
-
ctx.unresolved.push(`${ctx.describe(call)}: '${method}()' was removed; read \`(await migrator.status()).${method}\``);
|
|
554
|
-
}
|
|
555
|
-
else if (method === 'down' && !call.arguments.length) {
|
|
556
|
-
ctx.notes.push(`${ctx.describe(call)}: down() with no options reverts only the last migration now; pass \`{ step: Infinity }\` to revert them all`);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
/** Whether `member` is declared by uql-orm, so a `run` of another library's is left alone. */
|
|
560
|
-
function isUqlMember(member, ctx) {
|
|
561
|
-
const declarations = ctx.checker.getSymbolAtLocation(member.name)?.declarations ?? [];
|
|
562
|
-
return declarations.some((declaration) => declaration.getSourceFile().fileName.includes('/node_modules/uql-orm/'));
|
|
563
|
-
}
|
|
564
402
|
/** The migration builder's index methods, each with the position of the options it takes. */
|
|
565
403
|
const BUILDER_INDEX_METHODS = new Map([
|
|
566
404
|
['index', 1],
|
|
@@ -568,22 +406,22 @@ const BUILDER_INDEX_METHODS = new Map([
|
|
|
568
406
|
['addIndex', 1],
|
|
569
407
|
['createIndex', 2],
|
|
570
408
|
]);
|
|
571
|
-
/** The migration builder's `expr` helpers
|
|
572
|
-
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
|
+
]);
|
|
573
414
|
/**
|
|
574
415
|
* Rewrites the migration builder's `expr.<helper>()` default as the value or `raw` an entity declares, or reports
|
|
575
416
|
* one with no rewrite. Called only where the file imports `expr` from uql, so a variable of its own is left alone.
|
|
576
417
|
*/
|
|
577
418
|
function rewriteExprCall(call, ctx) {
|
|
578
419
|
const callee = call.expression;
|
|
579
|
-
if (!ts.isPropertyAccessExpression(callee) ||
|
|
580
|
-
!ts.isIdentifier(callee.expression) ||
|
|
581
|
-
callee.expression.text !== 'expr') {
|
|
420
|
+
if (!ts.isPropertyAccessExpression(callee) || identifierText(callee.expression) !== 'expr') {
|
|
582
421
|
return;
|
|
583
422
|
}
|
|
584
423
|
const helper = callee.name.text;
|
|
585
|
-
|
|
586
|
-
const value = helper === 'now' ? 'currentTimestamp' : EXPR_VALUES.has(helper) ? helper : undefined;
|
|
424
|
+
const value = EXPR_VALUES.get(helper);
|
|
587
425
|
const [sql] = call.arguments;
|
|
588
426
|
if (value) {
|
|
589
427
|
ctx.edits.push(replaced(call, value));
|
|
@@ -594,7 +432,7 @@ function rewriteExprCall(call, ctx) {
|
|
|
594
432
|
ctx.imports.set('raw', 'the raw`...`');
|
|
595
433
|
}
|
|
596
434
|
else {
|
|
597
|
-
ctx.
|
|
435
|
+
ctx.report(call, `expr.${helper}() is gone: declare the column with m.raw(...), or use a stamp on the entity`);
|
|
598
436
|
return;
|
|
599
437
|
}
|
|
600
438
|
ctx.rewritten.expr += 1;
|
|
@@ -604,11 +442,9 @@ function rewriteBuilderCall(call, ctx) {
|
|
|
604
442
|
const at = ts.isPropertyAccessExpression(call.expression)
|
|
605
443
|
? BUILDER_INDEX_METHODS.get(call.expression.name.text)
|
|
606
444
|
: undefined;
|
|
607
|
-
if (at
|
|
608
|
-
|
|
445
|
+
if (at !== undefined) {
|
|
446
|
+
rewriteIndexWhere(objectLiteral(call.arguments[at]), ctx);
|
|
609
447
|
}
|
|
610
|
-
const options = call.arguments[at];
|
|
611
|
-
rewriteIndexWhere(options && ts.isObjectLiteralExpression(options) ? options : undefined, ctx);
|
|
612
448
|
}
|
|
613
449
|
const UPSERT_CALLS = new Set(['upsertOne', 'upsertMany', 'upsertInto']);
|
|
614
450
|
/**
|
|
@@ -618,22 +454,18 @@ const UPSERT_CALLS = new Set(['upsertOne', 'upsertMany', 'upsertInto']);
|
|
|
618
454
|
*/
|
|
619
455
|
function rewriteUpsertCall(call, ctx) {
|
|
620
456
|
const callee = call.expression;
|
|
621
|
-
const name = ts.isPropertyAccessExpression(callee) ? callee.name : callee;
|
|
457
|
+
const name = identifierText(ts.isPropertyAccessExpression(callee) ? callee.name : callee);
|
|
622
458
|
const options = call.arguments[3];
|
|
623
459
|
const update = options && upsertUpdate(options);
|
|
624
|
-
if (
|
|
460
|
+
if (name && UPSERT_CALLS.has(name) && update) {
|
|
625
461
|
ctx.edits.push(replaced(options, update.getText()));
|
|
626
462
|
}
|
|
627
463
|
}
|
|
628
464
|
/** The value of options holding `update` alone, `{ update: value }` or `{ update }`. */
|
|
629
465
|
function upsertUpdate(options) {
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
}
|
|
634
|
-
return property && ts.isShorthandPropertyAssignment(property) && property.name.text === 'update'
|
|
635
|
-
? property.name
|
|
636
|
-
: 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);
|
|
637
469
|
}
|
|
638
470
|
/**
|
|
639
471
|
* `owner` is the declaring class's parameter and `target` the related one's, read off the `entity`
|
|
@@ -681,7 +513,7 @@ function isTextSearch(node) {
|
|
|
681
513
|
}
|
|
682
514
|
function rewriteAggregate(agg, ctx) {
|
|
683
515
|
if (ts.isObjectLiteralExpression(agg.parent) && propertyValue(agg.parent, '$select')) {
|
|
684
|
-
ctx.
|
|
516
|
+
ctx.report(agg, "merge '$agg' into the '$select' beside it");
|
|
685
517
|
return;
|
|
686
518
|
}
|
|
687
519
|
ctx.edits.push(replaced(agg.name, '$select'));
|
|
@@ -694,29 +526,15 @@ function rewriteAggregate(agg, ctx) {
|
|
|
694
526
|
}
|
|
695
527
|
}
|
|
696
528
|
}
|
|
697
|
-
/** The property assignments of an object literal, or none where `node` is not one. */
|
|
698
|
-
function objectProperties(node) {
|
|
699
|
-
return node && ts.isObjectLiteralExpression(node) ? node.properties.filter(ts.isPropertyAssignment) : [];
|
|
700
|
-
}
|
|
701
|
-
/** The elements of an array literal, or none where `node` is not one. */
|
|
702
|
-
function arrayElements(node) {
|
|
703
|
-
return node && ts.isArrayLiteralExpression(node) ? node.elements : [];
|
|
704
|
-
}
|
|
705
529
|
/**
|
|
706
|
-
* Drops `declare` from a decorated field
|
|
707
|
-
* nothing to decorate and rejects it ("Decorators are not valid here").
|
|
708
|
-
* 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").
|
|
709
532
|
*/
|
|
710
533
|
function dropDeclare(node, ctx) {
|
|
711
534
|
const modifier = node.modifiers?.find((m) => m.kind === ts.SyntaxKind.DeclareKeyword);
|
|
712
535
|
if (modifier) {
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
const text = node.getSourceFile().text;
|
|
716
|
-
let end = modifier.getEnd();
|
|
717
|
-
while (/\s/.test(text[end])) {
|
|
718
|
-
end += 1;
|
|
719
|
-
}
|
|
536
|
+
const rest = node.getSourceFile().text.slice(modifier.getEnd());
|
|
537
|
+
const end = modifier.getEnd() + rest.length - rest.trimStart().length;
|
|
720
538
|
ctx.edits.push({ start: modifier.getStart(), end, text: '' });
|
|
721
539
|
}
|
|
722
540
|
}
|
|
@@ -729,85 +547,23 @@ function unwrapRelationAlias(node, ctx) {
|
|
|
729
547
|
const declared = node.type;
|
|
730
548
|
if (declared &&
|
|
731
549
|
ts.isTypeReferenceNode(declared) &&
|
|
732
|
-
|
|
733
|
-
declared.typeName.text === 'Relation' &&
|
|
550
|
+
identifierText(declared.typeName) === 'Relation' &&
|
|
734
551
|
declared.typeArguments?.length === 1) {
|
|
735
552
|
ctx.edits.push(replaced(declared, declared.typeArguments[0].getText()));
|
|
736
553
|
ctx.rewritten.Relation += 1;
|
|
737
554
|
}
|
|
738
555
|
}
|
|
739
|
-
/** Removes a statement together with the rest of its line, so nothing is left blank behind it. */
|
|
740
|
-
function removeStatement(node) {
|
|
741
|
-
const source = node.getSourceFile();
|
|
742
|
-
const line = source.getLineAndCharacterOfPosition(node.getEnd()).line;
|
|
743
|
-
const nextLineStart = source.getLineStarts()[line + 1];
|
|
744
|
-
return { start: node.getStart(), end: nextLineStart ?? node.getEnd(), text: '' };
|
|
745
|
-
}
|
|
746
|
-
/**
|
|
747
|
-
* Names `uql-orm` no longer exports and that the codemod does remove: `Relation` once every usage was
|
|
748
|
-
* unwrapped, and `expr` once every default was rewritten. A removed *decorator* keeps its import on purpose (see
|
|
749
|
-
* {@link REMOVED_DECORATORS}).
|
|
750
|
-
*/
|
|
751
|
-
function deadImportNames(source, ctx) {
|
|
752
|
-
const imported = uqlImports(source, true);
|
|
753
|
-
// Every use of the name the file imports, the places no rewrite reaches included.
|
|
754
|
-
const uses = (name) => {
|
|
755
|
-
const element = imported.find((specifier) => importedName(specifier) === name);
|
|
756
|
-
return element ? usesOf(source, element.name, ctx.checker).length : 0;
|
|
757
|
-
};
|
|
758
|
-
const { rewritten } = ctx;
|
|
759
|
-
const relations = uses('Relation');
|
|
760
|
-
if (relations > rewritten.Relation) {
|
|
761
|
-
ctx.unresolved.push(`${source.fileName}: ${relations - rewritten.Relation} 'Relation<T>' reference(s) are somewhere this codemod ` +
|
|
762
|
-
'does not rewrite; unwrap them and drop the import by hand');
|
|
763
|
-
}
|
|
764
|
-
const names = ['Relation', 'expr'];
|
|
765
|
-
return new Set(names.filter((name) => rewritten[name] > 0 && rewritten[name] === uses(name)));
|
|
766
|
-
}
|
|
767
|
-
/**
|
|
768
|
-
* Removes `import 'reflect-metadata'`, since the polyfill only ever fed `design:type`, and each `dead` name
|
|
769
|
-
* from the imports {@link renameExports} left as written, having left the dead names out of the rest.
|
|
770
|
-
*/
|
|
771
|
-
function dropDeadImports(source, dead, rewritten, ctx) {
|
|
772
|
-
for (const statement of source.statements) {
|
|
773
|
-
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
774
|
-
continue;
|
|
775
|
-
}
|
|
776
|
-
if (statement.moduleSpecifier.text === 'reflect-metadata' && !statement.importClause) {
|
|
777
|
-
ctx.edits.push(removeStatement(statement));
|
|
778
|
-
continue;
|
|
779
|
-
}
|
|
780
|
-
const named = statement.importClause?.namedBindings;
|
|
781
|
-
if (!named || !ts.isNamedImports(named) || rewritten.has(statement)) {
|
|
782
|
-
continue;
|
|
783
|
-
}
|
|
784
|
-
const dropped = named.elements.filter((element) => dead.has(element.name.text));
|
|
785
|
-
if (dropped.length === named.elements.length && !statement.importClause?.name) {
|
|
786
|
-
ctx.edits.push(removeStatement(statement));
|
|
787
|
-
continue;
|
|
788
|
-
}
|
|
789
|
-
for (const element of dropped) {
|
|
790
|
-
ctx.edits.push(removeFromList(named.elements, element, source));
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
556
|
/** The names a key is read from without help, mirroring `NamedIdKey` in `uql-orm`. */
|
|
795
557
|
const CONVENTIONAL_ID_NAMES = new Set(['id', '_id', 'uuid']);
|
|
796
558
|
/** Whether the member is an `[idKey]?: ...` brand, matched by the name it is written under. */
|
|
797
559
|
function isIdKeyBrand(member) {
|
|
798
560
|
return (ts.isPropertyDeclaration(member) &&
|
|
799
561
|
ts.isComputedPropertyName(member.name) &&
|
|
800
|
-
|
|
801
|
-
member.name.expression.text === 'idKey');
|
|
562
|
+
identifierText(member.name.expression) === 'idKey');
|
|
802
563
|
}
|
|
803
564
|
/**
|
|
804
|
-
* Names the class's key with the `idKey` brand
|
|
805
|
-
*
|
|
806
|
-
* Only the class's own `@Id` properties are read, which is also what makes an overriding key right:
|
|
807
|
-
* a subclass that replaces an inherited `id` needs the brand precisely because the inherited name
|
|
808
|
-
* would otherwise be taken for the key.
|
|
809
|
-
*
|
|
810
|
-
* 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.
|
|
811
567
|
*/
|
|
812
568
|
function brandIdKey(node, ctx) {
|
|
813
569
|
const ids = node.members.filter(isIdProperty);
|
|
@@ -816,69 +572,43 @@ function brandIdKey(node, ctx) {
|
|
|
816
572
|
}
|
|
817
573
|
const names = ids.map((m) => propertyKey(m.name)).filter((name) => name !== undefined);
|
|
818
574
|
if (names.length !== ids.length) {
|
|
819
|
-
ctx.
|
|
575
|
+
ctx.report(node, "a key written as a computed name; add the 'idKey' brand by hand");
|
|
820
576
|
return;
|
|
821
577
|
}
|
|
822
578
|
if (names.length === 1 && CONVENTIONAL_ID_NAMES.has(names[0])) {
|
|
823
579
|
return;
|
|
824
580
|
}
|
|
825
|
-
const indent = ' '.repeat(node.getSourceFile().getLineAndCharacterOfPosition(ids[0].getStart()).character);
|
|
826
|
-
const brand = names.map(quoted).join(' | ');
|
|
827
581
|
const start = node.members.pos;
|
|
828
|
-
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(' | ')};` });
|
|
829
583
|
ctx.imports.set('idKey', 'the brand(s)');
|
|
830
584
|
}
|
|
831
585
|
/**
|
|
832
|
-
*
|
|
833
|
-
*
|
|
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.
|
|
834
588
|
*/
|
|
835
|
-
function addImports(source, rewritten, ctx) {
|
|
836
|
-
const imported = new Set(uqlImports(source).map(importedName));
|
|
837
|
-
const missing = [...ctx.imports].filter(([name]) => !imported.has(name));
|
|
838
|
-
if (!missing.length) {
|
|
839
|
-
return;
|
|
840
|
-
}
|
|
841
|
-
const anchor = uqlImportDeclarations(source).find(({ declaration }) => !rewritten.has(declaration));
|
|
842
|
-
if (!anchor) {
|
|
843
|
-
for (const [name, what] of missing) {
|
|
844
|
-
ctx.unresolved.push(`${source.fileName}: import '${name}' from 'uql-orm' for ${what} written here`);
|
|
845
|
-
}
|
|
846
|
-
return;
|
|
847
|
-
}
|
|
848
|
-
const names = missing.map(([name]) => name);
|
|
849
|
-
const typeOnly = anchor.declaration.importClause?.phaseModifier === ts.SyntaxKind.TypeKeyword;
|
|
850
|
-
ctx.edits.push(typeOnly
|
|
851
|
-
? inserted(anchor.declaration, `import { ${names.join(', ')} } from 'uql-orm';\n`)
|
|
852
|
-
: inserted(anchor.elements[0], names.map((name) => `${name}, `).join('')));
|
|
853
|
-
}
|
|
854
|
-
/** Everything the standard spec needs written onto one decorated property, and the options renamed since. */
|
|
855
589
|
function rewriteProperty(node, ctx) {
|
|
856
|
-
const owner =
|
|
590
|
+
const owner = ownerOf(node.parent);
|
|
857
591
|
const decorators = decoratorsOf(node);
|
|
858
592
|
for (const decorator of decorators) {
|
|
859
|
-
const name = decoratorName(decorator);
|
|
860
|
-
if (!name) {
|
|
861
|
-
continue;
|
|
862
|
-
}
|
|
593
|
+
const name = decoratorName(decorator) ?? '';
|
|
863
594
|
const options = decoratorOptions(decorator);
|
|
864
595
|
if (FIELD_DECORATORS.has(name)) {
|
|
865
596
|
if (name === 'Field') {
|
|
866
597
|
admitNull(options, node, ctx);
|
|
867
598
|
}
|
|
868
599
|
addFieldType(decorator, node, ctx);
|
|
869
|
-
// Options it cannot read may still hold `virtual`, so they are reported where they mention it.
|
|
870
600
|
if (options.kind === 'opaque' && /\bvirtual\b/.test(decorator.getText())) {
|
|
871
|
-
ctx.
|
|
601
|
+
ctx.report(node, options.reason);
|
|
872
602
|
}
|
|
873
603
|
renameSqlOption(options, 'virtual', 'computed', node, owner, ctx);
|
|
874
604
|
}
|
|
875
605
|
if (RELATION_DECORATORS.has(name)) {
|
|
876
606
|
addRelationEntity(decorator, node, ctx);
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
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)) {
|
|
882
612
|
addForeignKeyReference(options.node, propertyKey(node.name), owner, ctx, node);
|
|
883
613
|
}
|
|
884
614
|
}
|
|
@@ -889,34 +619,26 @@ function rewriteProperty(node, ctx) {
|
|
|
889
619
|
unwrapRelationAlias(node, ctx);
|
|
890
620
|
}
|
|
891
621
|
/**
|
|
892
|
-
* A column holds `null` unless `nullable: false` says otherwise,
|
|
893
|
-
*
|
|
894
|
-
* 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.
|
|
895
625
|
*/
|
|
896
626
|
function admitNull(options, node, ctx) {
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
return;
|
|
901
|
-
}
|
|
902
|
-
if (declaresNotNull(options) ||
|
|
627
|
+
if (options.kind === 'opaque' ||
|
|
628
|
+
!ctx.strictNullChecks ||
|
|
629
|
+
declaresNotNull(options) ||
|
|
903
630
|
findProperty(options, 'isId') ||
|
|
904
631
|
(findProperty(options, 'computed') && !findProperty(options, 'type'))) {
|
|
905
632
|
return;
|
|
906
633
|
}
|
|
907
634
|
if (!node.type) {
|
|
908
|
-
ctx.
|
|
909
|
-
return;
|
|
635
|
+
ctx.note(node, `declare '${propertyKey(node.name)}' as a type that admits 'null', which the column holds`);
|
|
910
636
|
}
|
|
911
|
-
if (admitsNull(node.type)) {
|
|
912
|
-
|
|
637
|
+
else if (!admitsNull(node.type)) {
|
|
638
|
+
ctx.edits.push(appended(node.type, ' | null'));
|
|
913
639
|
}
|
|
914
|
-
ctx.edits.push(appended(node.type, ' | null'));
|
|
915
640
|
}
|
|
916
|
-
/**
|
|
917
|
-
* The same null rule where `defineEntity` names the field: the property is on the class the call takes,
|
|
918
|
-
* so the declaration is found there rather than under a decorator.
|
|
919
|
-
*/
|
|
641
|
+
/** The same null rule where `defineEntity` names the field, on the property of the class the call takes. */
|
|
920
642
|
function admitNullOnDefined(field, owner, ctx) {
|
|
921
643
|
const key = propertyKey(field.name);
|
|
922
644
|
const declaration = key
|
|
@@ -949,135 +671,23 @@ function reportRemovedDecorators(node, ctx) {
|
|
|
949
671
|
const name = decoratorName(decorator);
|
|
950
672
|
const advice = name && REMOVED_DECORATORS.get(name);
|
|
951
673
|
if (advice) {
|
|
952
|
-
ctx.
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
/** Names an export that no longer exists, where it is imported from the package or one of its entries. */
|
|
957
|
-
function reportRemovedExports(source, ctx) {
|
|
958
|
-
for (const { declaration, entry, elements } of uqlImportDeclarations(source, true)) {
|
|
959
|
-
if (entry === 'uql-orm/util') {
|
|
960
|
-
const names = elements.filter((element) => !renamedTo(element, entry)).map(importedName);
|
|
961
|
-
if (names.length) {
|
|
962
|
-
ctx.unresolved.push(`${ctx.describe(declaration)}: 'uql-orm/util' was removed; its helpers are internal, so write your own \`${names.join('`, `')}\``);
|
|
963
|
-
}
|
|
964
|
-
continue;
|
|
965
|
-
}
|
|
966
|
-
const root = (MOVED_ENTRIES.get(entry) ?? entry) === 'uql-orm';
|
|
967
|
-
for (const element of elements) {
|
|
968
|
-
const name = importedName(element);
|
|
969
|
-
const removed = REMOVED_EXPORTS.get(name);
|
|
970
|
-
const internal = root ? INTERNAL_EXPORTS.get(name) : undefined;
|
|
971
|
-
if (removed) {
|
|
972
|
-
ctx.unresolved.push(`${ctx.describe(element)}: '${name}' was removed; ${removed}`);
|
|
973
|
-
}
|
|
974
|
-
else if (internal) {
|
|
975
|
-
ctx.unresolved.push(`${ctx.describe(element)}: '${name}' is internal; ${internal}`);
|
|
976
|
-
}
|
|
674
|
+
ctx.report(decorator, `'@${name}()' was removed; ${advice}`);
|
|
977
675
|
}
|
|
978
676
|
}
|
|
979
677
|
}
|
|
980
|
-
/**
|
|
981
|
-
* Rewrites each import naming a {@link RENAMED_EXPORTS} export, or from a {@link MOVED_ENTRIES} entry, renaming
|
|
982
|
-
* every use of it: a name moving entries gets an import from its new one, and a name the file already imports is dropped, as is a `dead`
|
|
983
|
-
* one. Returns the rewritten imports, which nothing else may edit.
|
|
984
|
-
*/
|
|
985
|
-
function renameExports(source, dead, ctx) {
|
|
986
|
-
const imports = uqlImportDeclarations(source, true);
|
|
987
|
-
const kept = imports.flatMap(({ entry, elements }) => elements.filter((element) => !renamedTo(element, entry)));
|
|
988
|
-
const bound = new Set(kept.map((element) => element.name.text));
|
|
989
|
-
const rewritten = new Set();
|
|
990
|
-
for (const { declaration, entry: imported, elements } of imports) {
|
|
991
|
-
const entry = MOVED_ENTRIES.get(imported) ?? imported;
|
|
992
|
-
if (entry === imported && !elements.some((element) => renamedTo(element, entry))) {
|
|
993
|
-
continue;
|
|
994
|
-
}
|
|
995
|
-
const byEntry = new Map([[entry, []]]);
|
|
996
|
-
for (const element of elements.filter(({ name }) => !dead.has(name.text))) {
|
|
997
|
-
const rename = renamedTo(element, entry);
|
|
998
|
-
const local = element.propertyName || !rename ? element.name.text : rename.to;
|
|
999
|
-
if (rename && !element.propertyName) {
|
|
1000
|
-
ctx.edits.push(...usesOf(source, element.name, ctx.checker).map((use) => replaced(use, rename.to)));
|
|
1001
|
-
}
|
|
1002
|
-
if (rename && bound.has(local)) {
|
|
1003
|
-
continue;
|
|
1004
|
-
}
|
|
1005
|
-
bound.add(local);
|
|
1006
|
-
const target = rename?.from ?? entry;
|
|
1007
|
-
const text = rename
|
|
1008
|
-
? `${element.isTypeOnly ? 'type ' : ''}${rename.to}${element.propertyName ? ` as ${local}` : ''}`
|
|
1009
|
-
: element.getText();
|
|
1010
|
-
byEntry.set(target, [...(byEntry.get(target) ?? []), text]);
|
|
1011
|
-
}
|
|
1012
|
-
const typeOnly = declaration.importClause?.phaseModifier === ts.SyntaxKind.TypeKeyword;
|
|
1013
|
-
const keyword = typeOnly ? 'import type' : 'import';
|
|
1014
|
-
const quote = declaration.moduleSpecifier.getText()[0];
|
|
1015
|
-
const statements = [...byEntry]
|
|
1016
|
-
.filter(([, names]) => names.length)
|
|
1017
|
-
.map(([from, names]) => `${keyword} { ${names.join(', ')} } from ${quote}${from}${quote};`);
|
|
1018
|
-
ctx.edits.push(statements.length ? replaced(declaration, statements.join('\n')) : removeStatement(declaration));
|
|
1019
|
-
rewritten.add(declaration);
|
|
1020
|
-
}
|
|
1021
|
-
return rewritten;
|
|
1022
|
-
}
|
|
1023
|
-
/** The rename `element` still needs, none where it already names the export at its entry. */
|
|
1024
|
-
function renamedTo(element, entry) {
|
|
1025
|
-
const name = importedName(element);
|
|
1026
|
-
const rename = RENAMED_EXPORTS.get(`${entry}#${name}`) ?? RENAMED_EXPORTS.get(name);
|
|
1027
|
-
return rename?.to === name && rename.from === entry ? undefined : rename;
|
|
1028
|
-
}
|
|
1029
|
-
/** Every identifier in the file bound to the same symbol as `name`, besides `name` itself. */
|
|
1030
|
-
function usesOf(source, name, checker) {
|
|
1031
|
-
const symbol = checker.getSymbolAtLocation(name);
|
|
1032
|
-
const uses = [];
|
|
1033
|
-
const visit = (node) => {
|
|
1034
|
-
if (ts.isIdentifier(node) && node !== name && node.text === name.text) {
|
|
1035
|
-
if (symbol && checker.getSymbolAtLocation(node) === symbol) {
|
|
1036
|
-
uses.push(node);
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
ts.forEachChild(node, visit);
|
|
1040
|
-
};
|
|
1041
|
-
visit(source);
|
|
1042
|
-
return uses;
|
|
1043
|
-
}
|
|
1044
|
-
/**
|
|
1045
|
-
* The file's named imports from `uql-orm`. `entries` adds the driver entries (`uql-orm/postgres`, ...),
|
|
1046
|
-
* for what is read by name only: `idKey` written into one would land on an entry that may not export it.
|
|
1047
|
-
*/
|
|
1048
|
-
function uqlImportDeclarations(source, entries = false) {
|
|
1049
|
-
return source.statements.flatMap((declaration) => {
|
|
1050
|
-
if (!ts.isImportDeclaration(declaration) || !ts.isStringLiteral(declaration.moduleSpecifier)) {
|
|
1051
|
-
return [];
|
|
1052
|
-
}
|
|
1053
|
-
const entry = declaration.moduleSpecifier.text;
|
|
1054
|
-
if (entry !== 'uql-orm' && !(entries && entry.startsWith('uql-orm/'))) {
|
|
1055
|
-
return [];
|
|
1056
|
-
}
|
|
1057
|
-
const bindings = declaration.importClause?.namedBindings;
|
|
1058
|
-
return bindings && ts.isNamedImports(bindings) ? [{ declaration, entry, elements: bindings.elements }] : [];
|
|
1059
|
-
});
|
|
1060
|
-
}
|
|
1061
|
-
function uqlImports(source, entries = false) {
|
|
1062
|
-
return uqlImportDeclarations(source, entries).flatMap(({ elements }) => elements);
|
|
1063
|
-
}
|
|
1064
|
-
/** The name an import specifier brings in, which is the original one where it was renamed. */
|
|
1065
|
-
function importedName(element) {
|
|
1066
|
-
return (element.propertyName ?? element.name).text;
|
|
1067
|
-
}
|
|
1068
678
|
/**
|
|
1069
679
|
* Rewrites `raw('sql')` into the tagged template, and a second alias argument into `.as()`, the callback
|
|
1070
680
|
* form's included. A computed string is left alone: a template cannot be built from a value not known here.
|
|
1071
681
|
*/
|
|
1072
|
-
function rewriteRawCall(node, ctx
|
|
1073
|
-
if (!ts.isCallExpression(node) ||
|
|
682
|
+
function rewriteRawCall(node, ctx) {
|
|
683
|
+
if (!ts.isCallExpression(node) || identifierText(node.expression) !== 'raw') {
|
|
1074
684
|
return;
|
|
1075
685
|
}
|
|
1076
686
|
const [expression, alias] = node.arguments;
|
|
1077
687
|
if (!expression || node.arguments.length > 2) {
|
|
1078
688
|
return;
|
|
1079
689
|
}
|
|
1080
|
-
const suffix = alias ? `.as(${alias.getText(
|
|
690
|
+
const suffix = alias ? `.as(${alias.getText()})` : '';
|
|
1081
691
|
if (ts.isStringLiteral(expression)) {
|
|
1082
692
|
ctx.edits.push(replaced(node, `${rawTag(expression.text)}${suffix}`));
|
|
1083
693
|
}
|
|
@@ -1086,170 +696,23 @@ function rewriteRawCall(node, ctx, source) {
|
|
|
1086
696
|
}
|
|
1087
697
|
}
|
|
1088
698
|
/**
|
|
1089
|
-
* Rewrites
|
|
1090
|
-
*
|
|
1091
|
-
* other use is reported. Returns `col` as a dead import once no use is left.
|
|
1092
|
-
*/
|
|
1093
|
-
function rewriteColumnRefs(source, ctx) {
|
|
1094
|
-
const element = uqlImports(source).find((use) => importedName(use) === 'col');
|
|
1095
|
-
if (!element) {
|
|
1096
|
-
return [];
|
|
1097
|
-
}
|
|
1098
|
-
const computed = new Map();
|
|
1099
|
-
const unread = [];
|
|
1100
|
-
for (const use of usesOf(source, element.name, ctx.checker)) {
|
|
1101
|
-
const call = ts.isCallExpression(use.parent) && use.parent.expression === use ? use.parent : undefined;
|
|
1102
|
-
const sql = call && ts.findAncestor(call, isRaw);
|
|
1103
|
-
const option = sql?.parent;
|
|
1104
|
-
const owner = option && ts.isPropertyAssignment(option) && isComputedOption(option) ? computedOwner(option) : undefined;
|
|
1105
|
-
const edit = call && !owner ? statementRefEdit(call, ctx) : undefined;
|
|
1106
|
-
if (call && sql && ts.isTaggedTemplateExpression(sql) && owner) {
|
|
1107
|
-
computed.set(sql, { owner, calls: [...(computed.get(sql)?.calls ?? []), call] });
|
|
1108
|
-
}
|
|
1109
|
-
else if (edit) {
|
|
1110
|
-
ctx.edits.push(edit);
|
|
1111
|
-
ctx.imports.set('refs', 'the refs(...)');
|
|
1112
|
-
}
|
|
1113
|
-
else {
|
|
1114
|
-
unread.push(use);
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
for (const [sql, { owner, calls }] of computed) {
|
|
1118
|
-
const edits = computedRefEdits(sql, calls, owner, ctx);
|
|
1119
|
-
if (edits) {
|
|
1120
|
-
ctx.edits.push(...edits);
|
|
1121
|
-
}
|
|
1122
|
-
else {
|
|
1123
|
-
unread.push(...calls);
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
for (const node of unread.sort((a, b) => a.getStart() - b.getStart())) {
|
|
1127
|
-
ctx.unresolved.push(`${ctx.describe(node)}: 'col' was removed; read the column off refs(Entity), which the codemod could not tell here`);
|
|
1128
|
-
}
|
|
1129
|
-
return unread.length ? [] : ['col'];
|
|
1130
|
-
}
|
|
1131
|
-
function isComputedOption(option) {
|
|
1132
|
-
const key = propertyKey(option.name);
|
|
1133
|
-
return key === 'computed' || key === 'virtual';
|
|
1134
|
-
}
|
|
1135
|
-
/** The entity a field's `computed` SQL is declared on: the decorated property's class, or `defineEntity`'s first argument. */
|
|
1136
|
-
function computedOwner(option) {
|
|
1137
|
-
const holder = ts.findAncestor(option, (node) => ts.isPropertyDeclaration(node) ||
|
|
1138
|
-
(ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'defineEntity'));
|
|
1139
|
-
if (holder && ts.isPropertyDeclaration(holder)) {
|
|
1140
|
-
return { param: paramFor(holder.parent.name?.text), entity: holder.parent };
|
|
1141
|
-
}
|
|
1142
|
-
const [entity] = holder?.arguments ?? [];
|
|
1143
|
-
return entity && { param: paramFor(ts.isIdentifier(entity) ? entity.text : undefined), entity };
|
|
1144
|
-
}
|
|
1145
|
-
/** `(product) => ` before a `computed` raw, and `product.x` for each `col('x')` in it, where every one names a member. */
|
|
1146
|
-
function computedRefEdits(sql, calls, owner, ctx) {
|
|
1147
|
-
const members = memberNames(owner.entity, ctx.checker);
|
|
1148
|
-
const names = calls.map((call) => columnMember(call, members));
|
|
1149
|
-
if (mentions(sql, owner.param) || !names.every((name) => name !== undefined)) {
|
|
1150
|
-
return undefined;
|
|
1151
|
-
}
|
|
1152
|
-
return [
|
|
1153
|
-
inserted(sql, `(${owner.param}) => `),
|
|
1154
|
-
...calls.map((call, at) => replaced(call, memberAccess(owner.param, names[at]))),
|
|
1155
|
-
];
|
|
1156
|
-
}
|
|
1157
|
-
/** `refs(Item).x` for a `col('x')` in a statement, where the entity its SQL renders against can be told. */
|
|
1158
|
-
function statementRefEdit(call, ctx) {
|
|
1159
|
-
const entity = statementEntity(call, ctx.checker);
|
|
1160
|
-
const name = entity && columnMember(call, propertyNames(entity.type));
|
|
1161
|
-
return entity && name !== undefined ? replaced(call, memberAccess(`refs(${entity.name})`, name)) : undefined;
|
|
1162
|
-
}
|
|
1163
|
-
/** The member a `col('x')` names, where its one argument is a literal naming one of `members`. */
|
|
1164
|
-
function columnMember(call, members) {
|
|
1165
|
-
const [name] = call.arguments;
|
|
1166
|
-
return call.arguments.length === 1 && ts.isStringLiteralLike(name) && members.has(name.text) ? name.text : undefined;
|
|
1167
|
-
}
|
|
1168
|
-
/** Whether `node` reads a binding named `name`, which a callback parameter of that name would shadow. */
|
|
1169
|
-
function mentions(node, name) {
|
|
1170
|
-
if (ts.isIdentifier(node)) {
|
|
1171
|
-
return node.text === name && !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node);
|
|
1172
|
-
}
|
|
1173
|
-
return ts.forEachChild(node, (child) => mentions(child, name) || undefined) ?? false;
|
|
1174
|
-
}
|
|
1175
|
-
/**
|
|
1176
|
-
* The entity a statement's SQL renders against at `node`: the one its query names, or a relation a `$populate`
|
|
1177
|
-
* reads there. Nothing where a function or a relation filter in between opens a scope of its own.
|
|
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.
|
|
1178
701
|
*/
|
|
1179
|
-
function statementEntity(node, checker) {
|
|
1180
|
-
const path = [];
|
|
1181
|
-
for (let at = node; at.parent; at = at.parent) {
|
|
1182
|
-
const { parent } = at;
|
|
1183
|
-
if (ts.isFunctionLike(parent)) {
|
|
1184
|
-
return undefined;
|
|
1185
|
-
}
|
|
1186
|
-
if (ts.isPropertyAssignment(parent)) {
|
|
1187
|
-
path.unshift(propertyKey(parent.name) ?? '');
|
|
1188
|
-
}
|
|
1189
|
-
const queried = ts.isCallExpression(parent) && parent.arguments.some((argument) => argument === at);
|
|
1190
|
-
const entity = queried ? queriedEntity(parent, checker) : undefined;
|
|
1191
|
-
if (entity) {
|
|
1192
|
-
return entityAlong(entity, path, node, checker);
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
return undefined;
|
|
1196
|
-
}
|
|
1197
|
-
/** The entity a call queries: its first argument's class, or the `$entity` its query names. */
|
|
1198
|
-
function queriedEntity(call, checker) {
|
|
1199
|
-
const [first] = call.arguments;
|
|
1200
|
-
const entity = first && ts.isObjectLiteralExpression(first) ? propertyValue(first, '$entity') : first;
|
|
1201
|
-
const instance = entity && checker.getTypeAtLocation(entity).getConstructSignatures()[0]?.getReturnType();
|
|
1202
|
-
return entity && ts.isIdentifier(entity) && instance ? { name: entity.text, type: instance } : undefined;
|
|
1203
|
-
}
|
|
1204
|
-
/** `entity` followed along `path`: into each relation a `$populate` names, and nowhere a relation filter opens. */
|
|
1205
|
-
function entityAlong(entity, path, node, checker) {
|
|
1206
|
-
let current = entity;
|
|
1207
|
-
for (const [at, key] of path.entries()) {
|
|
1208
|
-
const property = current.type.getProperty(key);
|
|
1209
|
-
const related = property && relatedClass(property, node, checker);
|
|
1210
|
-
if (!related) {
|
|
1211
|
-
continue;
|
|
1212
|
-
}
|
|
1213
|
-
const inScope = checker.getSymbolsInScope(node, ts.SymbolFlags.Class).some(({ name }) => name === related.name);
|
|
1214
|
-
if (path[at - 1] !== '$populate' || !inScope) {
|
|
1215
|
-
return undefined;
|
|
1216
|
-
}
|
|
1217
|
-
current = { name: related.name, type: checker.getDeclaredTypeOfSymbol(related) };
|
|
1218
|
-
}
|
|
1219
|
-
return current;
|
|
1220
|
-
}
|
|
1221
|
-
/** The class a relation property points at, one or many, or nothing where the property holds no class. */
|
|
1222
|
-
function relatedClass(property, node, checker) {
|
|
1223
|
-
const type = checker.getNonNullableType(checker.getTypeOfSymbolAtLocation(property, node));
|
|
1224
|
-
const symbol = (checker.getIndexTypeOfType(type, ts.IndexKind.Number) ?? type).getSymbol();
|
|
1225
|
-
return symbol && symbol.flags & ts.SymbolFlags.Class ? symbol : undefined;
|
|
1226
|
-
}
|
|
1227
|
-
/** Rewrites one source file for the standard decorator spec. */
|
|
1228
702
|
export function transformFile(source, checker, options = { strictNullChecks: true }) {
|
|
1229
|
-
const ctx =
|
|
1230
|
-
|
|
1231
|
-
strictNullChecks: options.strictNullChecks,
|
|
1232
|
-
edits: [],
|
|
1233
|
-
unresolved: [],
|
|
1234
|
-
notes: [],
|
|
1235
|
-
imports: new Map(),
|
|
1236
|
-
rewritten: { Relation: 0, expr: 0 },
|
|
1237
|
-
describe: (node) => `${source.fileName}:${lineOf(node) + 1}`,
|
|
1238
|
-
};
|
|
1239
|
-
const lineOf = (node) => source.getLineAndCharacterOfPosition(node.getStart()).line;
|
|
1240
|
-
const rewritesRaw = uqlImports(source).some((element) => importedName(element) === 'raw');
|
|
1241
|
-
// The builder's methods are known by name alone, so they are read only in a file that imports uql-orm.
|
|
703
|
+
const ctx = createContext(source, checker, options.strictNullChecks);
|
|
704
|
+
const rewritesRaw = uqlImport(source, 'raw') !== undefined;
|
|
1242
705
|
const importsUql = uqlImportDeclarations(source, true).length > 0;
|
|
1243
|
-
const importsExpr =
|
|
706
|
+
const importsExpr = uqlImport(source, 'expr', true) !== undefined;
|
|
1244
707
|
const visit = (node) => {
|
|
1245
708
|
reportRemovedDecorators(node, ctx);
|
|
1246
709
|
if (rewritesRaw) {
|
|
1247
|
-
rewriteRawCall(node, ctx
|
|
710
|
+
rewriteRawCall(node, ctx);
|
|
1248
711
|
}
|
|
1249
712
|
if (ts.isPropertyDeclaration(node)) {
|
|
1250
713
|
rewriteProperty(node, ctx);
|
|
1251
714
|
}
|
|
1252
|
-
if (ts.
|
|
715
|
+
if (ts.isClassLike(node)) {
|
|
1253
716
|
brandIdKey(node, ctx);
|
|
1254
717
|
rewriteClassDecorators(node, ctx);
|
|
1255
718
|
}
|
|
@@ -1258,8 +721,7 @@ export function transformFile(source, checker, options = { strictNullChecks: tru
|
|
|
1258
721
|
rewriteUpsertCall(node, ctx);
|
|
1259
722
|
if (importsUql) {
|
|
1260
723
|
rewriteBuilderCall(node, ctx);
|
|
1261
|
-
|
|
1262
|
-
reportMigratorCall(node, ctx);
|
|
724
|
+
rewriteMethodCall(node, ctx);
|
|
1263
725
|
}
|
|
1264
726
|
if (importsExpr) {
|
|
1265
727
|
rewriteExprCall(node, ctx);
|
|
@@ -1274,11 +736,11 @@ export function transformFile(source, checker, options = { strictNullChecks: tru
|
|
|
1274
736
|
ts.forEachChild(node, visit);
|
|
1275
737
|
};
|
|
1276
738
|
visit(source);
|
|
1277
|
-
const dead = new Set([...deadImportNames(
|
|
1278
|
-
const rewritten = renameExports(
|
|
1279
|
-
reportRemovedExports(
|
|
1280
|
-
dropDeadImports(
|
|
1281
|
-
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);
|
|
1282
744
|
return {
|
|
1283
745
|
fileName: source.fileName,
|
|
1284
746
|
text: applyEdits(source.getFullText(), ctx.edits),
|