uql-codemod 0.46.0 → 0.47.0
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 +2 -0
- package/dist/cli.js +6 -0
- package/dist/entitySql.d.ts +2 -0
- package/dist/entitySql.js +5 -1
- package/dist/index.js +3 -1
- package/dist/transform.d.ts +3 -1
- package/dist/transform.js +171 -25
- package/package.json +1 -1
package/LICENSE.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2015-present UQL Contributors
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -32,7 +32,8 @@ Only `--flag=value` is read, and anything unrecognised is an error. It exits `0`
|
|
|
32
32
|
- A partial-index `where` string to `raw`, importing it, on entities and in the migration builder.
|
|
33
33
|
- `col('cost')` to the ref it names, ``(product) => raw`${product.cost}` `` in a `computed` and `refs(Item).cost` in a statement, and `$lock: { wait: 'skip' }` to `{ $wait: 'skip' }`, or to `true` for `{}` and a `'block'` wait.
|
|
34
34
|
- An aggregate's `$agg` to `$select`, and each field it or `$text`'s `$fields` names to a key: `{ $sum: { amount: true } }`.
|
|
35
|
-
- Renamed exports, at the import and every use: `QueryWhereMap` to `QueryWhere`, `RelationKeyMap` to `KeyMap`, `FilterCondition` to `FilterWhere`, `SqlMigrationModuleOptions` to `MigrationModuleOptions`, `buildSqlQuerierMigrationModule` to `buildMigrationModule`, `QueryDialect` to `SqlQueryDialect`, `EngineFeatures` to `DialectFeatures`, `KnownMigratorDialect` to `DialectName`, `D1Preparer`/`D1Database` to `D1Queryable`, and the removed driver classes to the one each extended: `PgDialect`/`NeonDialect`/`PgliteDialect` to `PostgresDialect`, `CrdbQuerier`/`NeonQuerier` to `PgQuerier`, `LibsqlQuerier`/`TursoQuerier` to `HranaQuerier`, `TursoLocalQuerier` to `SqliteQuerier` and `TursoDatabase` to `SqliteDatabase`, `MySql2Dialect` to `MySqlDialect`, `MongodbNativeDialect` to `MongoDialect`, moving the import to the entry that exports it; and `MongoQuerier`, `isMongoQuerier
|
|
35
|
+
- Renamed exports, at the import and every use: `QueryWhereMap` to `QueryWhere`, `RelationKeyMap` to `KeyMap`, `FilterCondition` to `FilterWhere`, `SqlMigrationModuleOptions` to `MigrationModuleOptions`, `buildSqlQuerierMigrationModule` to `buildMigrationModule`, `QueryDialect` to `SqlQueryDialect`, `EngineFeatures` to `DialectFeatures`, `KnownMigratorDialect` to `DialectName`, `D1Preparer`/`D1Database` to `D1Queryable`, and the removed driver classes to the one each extended: `PgDialect`/`NeonDialect`/`PgliteDialect` to `PostgresDialect`, `CrdbQuerier`/`NeonQuerier` to `PgQuerier`, `LibsqlQuerier`/`TursoQuerier` to `HranaQuerier`, `TursoLocalQuerier` to `SqliteQuerier` and `TursoDatabase` to `SqliteDatabase`, `MySql2Dialect` to `MySqlDialect`, `MongodbNativeDialect` to `MongoDialect`, `Sqlite3QuerierPool` to `SqliteQuerierPool`, the migration builder's `I`-prefixed types to the unprefixed name, moving the import to the entry that exports it; and `MongoQuerier`, `isMongoQuerier` and `MongoSchemaIntrospector` to `uql-orm/mongodb`.
|
|
36
|
+
- Imports off `uql-orm/maria`, `uql-orm/mongo` and `uql-orm/bunSql` to `uql-orm/mariadb`, `uql-orm/mongodb` and `uql-orm/bun-sql`, and off `uql-orm/type`, `uql-orm/dialect`, `uql-orm/entity`, `uql-orm/querier` and `uql-orm/namingStrategy` to `uql-orm`.
|
|
36
37
|
- The `[idKey]?: '...'` brand, and its import, on a key not called `id`, `_id` or `uuid` and on every composite.
|
|
37
38
|
- `import 'reflect-metadata'`, and `experimentalDecorators`/`emitDecoratorMetadata` in `tsconfig.json`, comments and formatting kept.
|
|
38
39
|
|
|
@@ -42,7 +43,7 @@ Reported with what to do, never guessed:
|
|
|
42
43
|
|
|
43
44
|
- `target: esnext`, the one target that leaves decorators untransformed, and a value inherited through `extends`.
|
|
44
45
|
- `@Log()`, `@Serialized()`, `@Transactional()` and `@InjectQuerier()`: a `@Transactional()` method becomes a `pool.transaction()` around its body, and only you know which pool.
|
|
45
|
-
- An export removed with no one-to-one replacement (`setQuerierPool`, `getQuerier`, `augmentWhere`, `AbstractPgQuerier`, ...).
|
|
46
|
+
- An export removed with no one-to-one replacement (`setQuerierPool`, `getQuerier`, `augmentWhere`, `AbstractPgQuerier`, ...), an internal the root no longer exports (`AbstractSqlQuerier`, `fieldOf`, ...), the migration storages, and any import from `uql-orm/util` the root does not export (`raw`, `refs`, ... move to `uql-orm`).
|
|
46
47
|
- A value it cannot read: options passed as a variable or a spread, a list that is not a literal, a key under a computed name.
|
|
47
48
|
- A to-one onto a composite key: declare a column per key and pair each with it in `references`.
|
|
48
49
|
- A to-one whose `<relation>Id` is not a declared column: an undecorated property, a `defineEntity` whose `fields` leave it out, or a `defineRelation` whose entity has no such member.
|
package/dist/bin.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from './cli.js';
|
|
3
3
|
import { run } from './index.js';
|
|
4
|
-
const USAGE = 'usage: uql-codemod [--project=<path>] [--include=<a,b>] [--dry-run]';
|
|
4
|
+
const USAGE = 'usage: uql-codemod [--project=<path>] [--include=<a,b>] [--dry-run] [--help]';
|
|
5
5
|
// Exit 2 is "could not start", so a script can tell it apart from exit 1, "there is work left".
|
|
6
6
|
const stop = (message) => {
|
|
7
7
|
console.error(message);
|
|
8
8
|
process.exit(2);
|
|
9
9
|
};
|
|
10
|
-
const { options, errors } = parseArgs(process.argv.slice(2));
|
|
10
|
+
const { options, help, errors } = parseArgs(process.argv.slice(2));
|
|
11
|
+
if (help) {
|
|
12
|
+
console.log(USAGE);
|
|
13
|
+
process.exit(0);
|
|
14
|
+
}
|
|
11
15
|
if (errors.length) {
|
|
12
16
|
stop([...errors, USAGE].join('\n'));
|
|
13
17
|
}
|
|
@@ -19,8 +23,11 @@ for (const line of summary.notes) {
|
|
|
19
23
|
for (const line of summary.unresolved) {
|
|
20
24
|
console.error(`needs a decision: ${line}`);
|
|
21
25
|
}
|
|
26
|
+
for (const file of summary.changed) {
|
|
27
|
+
console.log(`${options.dryRun ? 'would change' : 'changed'}: ${file}`);
|
|
28
|
+
}
|
|
22
29
|
console.log(`${summary.changed.length} file(s) ${options.dryRun ? 'would change' : 'changed'}`);
|
|
23
30
|
if (summary.unresolved.length) {
|
|
24
|
-
console.error(`${summary.unresolved.length}
|
|
31
|
+
console.error(`${summary.unresolved.length} item(s) need a decision; see above`);
|
|
25
32
|
process.exit(1);
|
|
26
33
|
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { RunOptions } from './index.js';
|
|
2
2
|
export type ParsedArgs = {
|
|
3
3
|
readonly options: RunOptions;
|
|
4
|
+
/** Whether to print the usage and stop. */
|
|
5
|
+
readonly help: boolean;
|
|
4
6
|
/** Why the arguments were rejected. Nothing runs while this is non-empty. */
|
|
5
7
|
readonly errors: readonly string[];
|
|
6
8
|
};
|
package/dist/cli.js
CHANGED
|
@@ -11,11 +11,16 @@ export function parseArgs(argv) {
|
|
|
11
11
|
const errors = [];
|
|
12
12
|
const values = new Map();
|
|
13
13
|
let dryRun = false;
|
|
14
|
+
let help = false;
|
|
14
15
|
for (const arg of argv) {
|
|
15
16
|
if (arg === '--dry-run') {
|
|
16
17
|
dryRun = true;
|
|
17
18
|
continue;
|
|
18
19
|
}
|
|
20
|
+
if (arg === '--help' || arg === '-h') {
|
|
21
|
+
help = true;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
19
24
|
const separator = arg.indexOf('=');
|
|
20
25
|
const name = arg.slice(2, separator === -1 ? undefined : separator);
|
|
21
26
|
if (!arg.startsWith('--') || !VALUE_FLAGS.has(name)) {
|
|
@@ -35,6 +40,7 @@ export function parseArgs(argv) {
|
|
|
35
40
|
dryRun,
|
|
36
41
|
include: values.get('include')?.split(',').filter(Boolean),
|
|
37
42
|
},
|
|
43
|
+
help,
|
|
38
44
|
errors,
|
|
39
45
|
};
|
|
40
46
|
}
|
package/dist/entitySql.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export declare function isRaw(node: ts.Node): node is RawSql;
|
|
|
7
7
|
export declare function columnExpressions(list: ts.Expression | undefined): readonly RawSql[];
|
|
8
8
|
/** SQL text as a `raw` tagged template, escaping what would end or interpolate it. */
|
|
9
9
|
export declare function rawTag(sql: string): string;
|
|
10
|
+
/** `sql` as a template literal reading the same text: a backslash, a backtick and a `${` escaped. */
|
|
11
|
+
export declare function templateOf(sql: string): string;
|
|
10
12
|
/** A partial-index `where` string as `raw`, or nothing for a template that interpolates, whose values `raw` would bind. */
|
|
11
13
|
export declare function rawWhereEdit(where: ts.Expression): Edit | undefined;
|
|
12
14
|
/**
|
package/dist/entitySql.js
CHANGED
|
@@ -20,7 +20,11 @@ export function columnExpressions(list) {
|
|
|
20
20
|
}
|
|
21
21
|
/** SQL text as a `raw` tagged template, escaping what would end or interpolate it. */
|
|
22
22
|
export function rawTag(sql) {
|
|
23
|
-
return `raw
|
|
23
|
+
return `raw${templateOf(sql)}`;
|
|
24
|
+
}
|
|
25
|
+
/** `sql` as a template literal reading the same text: a backslash, a backtick and a `${` escaped. */
|
|
26
|
+
export function templateOf(sql) {
|
|
27
|
+
return `\`${sql.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${')}\``;
|
|
24
28
|
}
|
|
25
29
|
/** A partial-index `where` string as `raw`, or nothing for a template that interpolates, whose values `raw` would bind. */
|
|
26
30
|
export function rawWhereEdit(where) {
|
package/dist/index.js
CHANGED
|
@@ -29,13 +29,15 @@ export async function run({ project, dryRun, include }) {
|
|
|
29
29
|
noEmit: true,
|
|
30
30
|
});
|
|
31
31
|
const checker = program.getTypeChecker();
|
|
32
|
+
const { strictNullChecks, strict } = program.getCompilerOptions();
|
|
33
|
+
const nulls = { strictNullChecks: strictNullChecks ?? strict ?? false };
|
|
32
34
|
const results = [];
|
|
33
35
|
for (const source of program.getSourceFiles()) {
|
|
34
36
|
if (source.isDeclarationFile || /[/\\]node_modules[/\\]/.test(source.fileName))
|
|
35
37
|
continue;
|
|
36
38
|
if (include?.length && !include.some((fragment) => source.fileName.includes(fragment)))
|
|
37
39
|
continue;
|
|
38
|
-
const result = transformFile(source, checker);
|
|
40
|
+
const result = transformFile(source, checker, nulls);
|
|
39
41
|
if (result.changed || result.unresolved.length || result.notes.length)
|
|
40
42
|
results.push(result);
|
|
41
43
|
}
|
package/dist/transform.d.ts
CHANGED
|
@@ -9,4 +9,6 @@ export type FileResult = {
|
|
|
9
9
|
readonly notes: readonly string[];
|
|
10
10
|
};
|
|
11
11
|
/** Rewrites one source file for the standard decorator spec. */
|
|
12
|
-
export declare function transformFile(source: ts.SourceFile, checker: ts.TypeChecker
|
|
12
|
+
export declare function transformFile(source: ts.SourceFile, checker: ts.TypeChecker, options?: {
|
|
13
|
+
readonly strictNullChecks: boolean;
|
|
14
|
+
}): FileResult;
|
package/dist/transform.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
2
|
import { appended, applyEdits, inserted, removeFromList, replaced } from './edits.js';
|
|
3
|
-
import { columnExpressions, handNamedColumns, isRaw, rawTag, rawWhereEdit } from './entitySql.js';
|
|
3
|
+
import { columnExpressions, handNamedColumns, isRaw, rawTag, rawWhereEdit, templateOf } from './entitySql.js';
|
|
4
4
|
import { fieldTypeFor, isBrandedString, relationTargetFor } from './fieldType.js';
|
|
5
5
|
import { entityGetterTarget, fieldKeysEdits, isCallback, keyListEdits, mappedByEdits, memberAccess, paramFor, propertyKey, propertyValue, quoted, referencesEdits, } from './keyMaps.js';
|
|
6
6
|
const FIELD_DECORATORS = new Set(['Field', 'Id']);
|
|
@@ -56,14 +56,33 @@ const REMOVED_EXPORTS = new Map([
|
|
|
56
56
|
'POSTGRES_WIRE_DRIVER_CAPABILITIES',
|
|
57
57
|
'pass `driverCapabilities: { nativeArrays: false, explicitJsonCast: true }` to the dialect',
|
|
58
58
|
],
|
|
59
|
-
['MysqlLikeSqlDialect', 'extend `MySqlDialect` (`uql-orm/mysql`) or `MariaDialect` (`uql-orm/
|
|
59
|
+
['MysqlLikeSqlDialect', 'extend `MySqlDialect` (`uql-orm/mysql`) or `MariaDialect` (`uql-orm/mariadb`)'],
|
|
60
60
|
['D1Meta', "uql reads `D1Result['meta']`; the rest of a binding is typed by `@cloudflare/workers-types`"],
|
|
61
61
|
['D1ExecResult', 'the rest of a binding is typed by `@cloudflare/workers-types`'],
|
|
62
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`']),
|
|
63
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
|
+
];
|
|
64
83
|
/**
|
|
65
|
-
* Exports renamed or moved and nothing else
|
|
66
|
-
*
|
|
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.
|
|
67
86
|
*/
|
|
68
87
|
const RENAMED_EXPORTS = new Map([
|
|
69
88
|
['QueryWhereMap', { to: 'QueryWhere' }],
|
|
@@ -75,7 +94,7 @@ const RENAMED_EXPORTS = new Map([
|
|
|
75
94
|
['CrdbQuerier', { to: 'PgQuerier', from: 'uql-orm/postgres' }],
|
|
76
95
|
['NeonQuerier', { to: 'PgQuerier', from: 'uql-orm/postgres' }],
|
|
77
96
|
['MySql2Dialect', { to: 'MySqlDialect', from: 'uql-orm/mysql' }],
|
|
78
|
-
['MongodbNativeDialect', { to: 'MongoDialect', from: 'uql-orm/
|
|
97
|
+
['MongodbNativeDialect', { to: 'MongoDialect', from: 'uql-orm/mongodb' }],
|
|
79
98
|
['LibsqlQuerier', { to: 'HranaQuerier', from: 'uql-orm/sqlite' }],
|
|
80
99
|
['TursoQuerier', { to: 'HranaQuerier', from: 'uql-orm/sqlite' }],
|
|
81
100
|
['TursoLocalQuerier', { to: 'SqliteQuerier', from: 'uql-orm/sqlite' }],
|
|
@@ -87,10 +106,43 @@ const RENAMED_EXPORTS = new Map([
|
|
|
87
106
|
['KnownMigratorDialect', { to: 'DialectName', from: 'uql-orm' }],
|
|
88
107
|
['D1Preparer', { to: 'D1Queryable' }],
|
|
89
108
|
['D1Database', { to: 'D1Queryable', from: 'uql-orm/d1' }],
|
|
90
|
-
['MongoQuerier', { to: 'MongoQuerier', from: 'uql-orm/
|
|
91
|
-
['isMongoQuerier', { to: 'isMongoQuerier', from: 'uql-orm/
|
|
92
|
-
['
|
|
93
|
-
['
|
|
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'],
|
|
94
146
|
]);
|
|
95
147
|
/** A node's decorators, or none where it is a kind that cannot carry them. */
|
|
96
148
|
function decoratorsOf(node) {
|
|
@@ -216,8 +268,8 @@ function addFieldType(decorator, node, ctx) {
|
|
|
216
268
|
const type = addInferredOption(decorator, node, ctx, {
|
|
217
269
|
option: 'type',
|
|
218
270
|
// A `references` field deliberately has no `type`: the column resolves from the primary key it
|
|
219
|
-
// points at, which also carries that key's `columnType` and length.
|
|
220
|
-
satisfiedBy: ['type', 'references'],
|
|
271
|
+
// points at, which also carries that key's `columnType` and length. A `computed` one is its SQL.
|
|
272
|
+
satisfiedBy: ['type', 'references', 'computed'],
|
|
221
273
|
infer: fieldTypeFor,
|
|
222
274
|
});
|
|
223
275
|
// A branded id type (`type UUID = `${string}-${string}-...``) erased to `String` under reflection, so
|
|
@@ -440,6 +492,75 @@ function rewriteIndexWhere(options, ctx) {
|
|
|
440
492
|
function isStringTyped(type) {
|
|
441
493
|
return type.isUnion() ? type.types.every(isStringTyped) : Boolean(type.flags & ts.TypeFlags.StringLike);
|
|
442
494
|
}
|
|
495
|
+
/**
|
|
496
|
+
* A querier's `run`/`all` handed SQL as a string, which now takes a tagged statement: a literal becomes
|
|
497
|
+
* `run`...``, SQL built at run time `raw.text`, which splices as the string did. One passed with values is reported.
|
|
498
|
+
*/
|
|
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
|
+
}
|
|
504
|
+
const [sql, ...values] = call.arguments;
|
|
505
|
+
if (!sql || !isStringTyped(ctx.checker.getTypeAtLocation(sql))) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (values.length) {
|
|
509
|
+
ctx.unresolved.push(`${ctx.describe(call)}: ${callee.name.text}() takes one raw statement: write the values into it, raw\`... \${value}\`, which binds them`);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (ts.isStringLiteral(sql) || ts.isNoSubstitutionTemplateLiteral(sql)) {
|
|
513
|
+
const typeArguments = call.typeArguments ? `<${call.typeArguments.map((type) => type.getText()).join(', ')}>` : '';
|
|
514
|
+
ctx.edits.push(replaced(call, `${callee.getText()}${typeArguments}${templateOf(sql.text)}`));
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (ts.isTemplateExpression(sql)) {
|
|
518
|
+
ctx.notes.push(`${ctx.describe(call)}: the values in this template are spliced into the SQL; write it as raw\`...\` to bind them`);
|
|
519
|
+
}
|
|
520
|
+
ctx.edits.push(replaced(sql, `raw.text(${sql.getText()})`));
|
|
521
|
+
ctx.imports.set('raw', 'the raw.text(...)');
|
|
522
|
+
}
|
|
523
|
+
const SQL_METHODS = new Set(['run', 'all']);
|
|
524
|
+
/** `const { id } = await q.upsertOne(...)`, which resolves to the id now, as the id: other keys are reported. */
|
|
525
|
+
function rewriteUpsertResult(declaration, ctx) {
|
|
526
|
+
const { name, initializer } = declaration;
|
|
527
|
+
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)) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const [element] = name.elements;
|
|
537
|
+
const key = element && (element.propertyName ?? element.name);
|
|
538
|
+
if (name.elements.length === 1 && key && ts.isIdentifier(key) && key.text === 'id' && ts.isIdentifier(element.name)) {
|
|
539
|
+
ctx.edits.push(replaced(name, element.name.text));
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
ctx.unresolved.push(`${ctx.describe(declaration)}: upsertOne() resolves to the id; 'created' and 'changes' are gone`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
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
|
+
}
|
|
443
564
|
/** The migration builder's index methods, each with the position of the options it takes. */
|
|
444
565
|
const BUILDER_INDEX_METHODS = new Map([
|
|
445
566
|
['index', 1],
|
|
@@ -717,14 +838,18 @@ function addImports(source, rewritten, ctx) {
|
|
|
717
838
|
if (!missing.length) {
|
|
718
839
|
return;
|
|
719
840
|
}
|
|
720
|
-
const anchor = uqlImportDeclarations(source).find(({ declaration }) => !rewritten.has(declaration))
|
|
841
|
+
const anchor = uqlImportDeclarations(source).find(({ declaration }) => !rewritten.has(declaration));
|
|
721
842
|
if (!anchor) {
|
|
722
843
|
for (const [name, what] of missing) {
|
|
723
844
|
ctx.unresolved.push(`${source.fileName}: import '${name}' from 'uql-orm' for ${what} written here`);
|
|
724
845
|
}
|
|
725
846
|
return;
|
|
726
847
|
}
|
|
727
|
-
|
|
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('')));
|
|
728
853
|
}
|
|
729
854
|
/** Everything the standard spec needs written onto one decorated property, and the options renamed since. */
|
|
730
855
|
function rewriteProperty(node, ctx) {
|
|
@@ -771,7 +896,7 @@ function rewriteProperty(node, ctx) {
|
|
|
771
896
|
function admitNull(options, node, ctx) {
|
|
772
897
|
// Options it cannot read may state `nullable` themselves, and appending to the property would then
|
|
773
898
|
// contradict the column. `addFieldType` already reports the same options, so this adds no second note.
|
|
774
|
-
if (options.kind === 'opaque') {
|
|
899
|
+
if (options.kind === 'opaque' || !ctx.strictNullChecks) {
|
|
775
900
|
return;
|
|
776
901
|
}
|
|
777
902
|
if (declaresNotNull(options) ||
|
|
@@ -830,17 +955,31 @@ function reportRemovedDecorators(node, ctx) {
|
|
|
830
955
|
}
|
|
831
956
|
/** Names an export that no longer exists, where it is imported from the package or one of its entries. */
|
|
832
957
|
function reportRemovedExports(source, ctx) {
|
|
833
|
-
for (const
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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
|
+
}
|
|
838
977
|
}
|
|
839
978
|
}
|
|
840
979
|
}
|
|
841
980
|
/**
|
|
842
|
-
* Rewrites each import naming a {@link RENAMED_EXPORTS} export,
|
|
843
|
-
* entries gets an import from its new one, and a name the file already imports is dropped, as is a `dead`
|
|
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`
|
|
844
983
|
* one. Returns the rewritten imports, which nothing else may edit.
|
|
845
984
|
*/
|
|
846
985
|
function renameExports(source, dead, ctx) {
|
|
@@ -848,8 +987,9 @@ function renameExports(source, dead, ctx) {
|
|
|
848
987
|
const kept = imports.flatMap(({ entry, elements }) => elements.filter((element) => !renamedTo(element, entry)));
|
|
849
988
|
const bound = new Set(kept.map((element) => element.name.text));
|
|
850
989
|
const rewritten = new Set();
|
|
851
|
-
for (const { declaration, entry, elements } of imports) {
|
|
852
|
-
|
|
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))) {
|
|
853
993
|
continue;
|
|
854
994
|
}
|
|
855
995
|
const byEntry = new Map([[entry, []]]);
|
|
@@ -883,7 +1023,7 @@ function renameExports(source, dead, ctx) {
|
|
|
883
1023
|
/** The rename `element` still needs, none where it already names the export at its entry. */
|
|
884
1024
|
function renamedTo(element, entry) {
|
|
885
1025
|
const name = importedName(element);
|
|
886
|
-
const rename = RENAMED_EXPORTS.get(name);
|
|
1026
|
+
const rename = RENAMED_EXPORTS.get(`${entry}#${name}`) ?? RENAMED_EXPORTS.get(name);
|
|
887
1027
|
return rename?.to === name && rename.from === entry ? undefined : rename;
|
|
888
1028
|
}
|
|
889
1029
|
/** Every identifier in the file bound to the same symbol as `name`, besides `name` itself. */
|
|
@@ -1085,9 +1225,10 @@ function relatedClass(property, node, checker) {
|
|
|
1085
1225
|
return symbol && symbol.flags & ts.SymbolFlags.Class ? symbol : undefined;
|
|
1086
1226
|
}
|
|
1087
1227
|
/** Rewrites one source file for the standard decorator spec. */
|
|
1088
|
-
export function transformFile(source, checker) {
|
|
1228
|
+
export function transformFile(source, checker, options = { strictNullChecks: true }) {
|
|
1089
1229
|
const ctx = {
|
|
1090
1230
|
checker,
|
|
1231
|
+
strictNullChecks: options.strictNullChecks,
|
|
1091
1232
|
edits: [],
|
|
1092
1233
|
unresolved: [],
|
|
1093
1234
|
notes: [],
|
|
@@ -1117,6 +1258,8 @@ export function transformFile(source, checker) {
|
|
|
1117
1258
|
rewriteUpsertCall(node, ctx);
|
|
1118
1259
|
if (importsUql) {
|
|
1119
1260
|
rewriteBuilderCall(node, ctx);
|
|
1261
|
+
rewriteSqlCall(node, ctx);
|
|
1262
|
+
reportMigratorCall(node, ctx);
|
|
1120
1263
|
}
|
|
1121
1264
|
if (importsExpr) {
|
|
1122
1265
|
rewriteExprCall(node, ctx);
|
|
@@ -1125,6 +1268,9 @@ export function transformFile(source, checker) {
|
|
|
1125
1268
|
if (ts.isPropertyAssignment(node)) {
|
|
1126
1269
|
rewriteStatementKeys(node, ctx);
|
|
1127
1270
|
}
|
|
1271
|
+
if (importsUql && ts.isVariableDeclaration(node)) {
|
|
1272
|
+
rewriteUpsertResult(node, ctx);
|
|
1273
|
+
}
|
|
1128
1274
|
ts.forEachChild(node, visit);
|
|
1129
1275
|
};
|
|
1130
1276
|
visit(source);
|