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/imports.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { inserted, removeFromList, removeStatement, replaced } from './edits.js';
|
|
3
|
+
/**
|
|
4
|
+
* Exports that no longer exist, mapped to what to do instead. Reported rather than rewritten: what replaces
|
|
5
|
+
* each is decided at its call site.
|
|
6
|
+
*/
|
|
7
|
+
const REMOVED_EXPORTS = new Map([
|
|
8
|
+
[
|
|
9
|
+
'setQuerierPool',
|
|
10
|
+
'pass the pool where it is used: `createFetchHandler({ pool, include })`, `querierMiddleware({ pool, include })`',
|
|
11
|
+
],
|
|
12
|
+
['getQuerierPool', 'take the pool from the module that builds it, or from Nest DI'],
|
|
13
|
+
['getQuerier', 'use `pool.withQuerier(...)` / `pool.transaction(...)`, which release the connection'],
|
|
14
|
+
['ClientQuerierPool', "construct the `HttpQuerier` where it is used, `new HttpQuerier('/api')`"],
|
|
15
|
+
['QueryWhereFieldMap', 'use `QueryWhere`'],
|
|
16
|
+
['QueryStreamProjected', 'use `QueryProjected`'],
|
|
17
|
+
['RelationMappedBy', "a 'mappedBy' is `(keys: KeyMap<E>) => Key<E>` now"],
|
|
18
|
+
['RelationKeyMapper', 'it is `(keys: KeyMap<E>) => Key<E>` now'],
|
|
19
|
+
['augmentWhere', 'spread the two maps: `{ ...where, ...extra }`'],
|
|
20
|
+
['buildQueryWhereAsMap', 'a `$where` is a map already; name the key for ids: `{ id: [1, 2] }`'],
|
|
21
|
+
['AbstractPgQuerier', 'every pg-compatible pool returns `PgQuerier` (`uql-orm/postgres`), which is concrete'],
|
|
22
|
+
['AbstractHranaQuerierPool', 'extend `AbstractSqlQuerierPool` and hand every querier a `HranaQuerier`'],
|
|
23
|
+
[
|
|
24
|
+
'PreparedSqliteQuerier',
|
|
25
|
+
'use `SqliteQuerier`, which takes any driver that prepares, or extend `AbstractSqliteQuerier`',
|
|
26
|
+
],
|
|
27
|
+
['toSqliteBindValues', 'pass the values as they are: every SQLite driver binds a `SqliteBindValue[]`'],
|
|
28
|
+
['libsqlUseRemoteForMigrations', '`LibsqlQuerierPool.getMigrationQuerier()` decides it'],
|
|
29
|
+
['SqlCallback', 'use `EntitySql<E>`, which holds the callback form'],
|
|
30
|
+
[
|
|
31
|
+
'IndexColumnOptions',
|
|
32
|
+
"an entity's index entry is `EntityIndexColumnInput<E>`, the migration builder's `IndexColumnInput`",
|
|
33
|
+
],
|
|
34
|
+
['isKnownMigratorDialect', 'every dialect has a migrator, so it always held: drop the check'],
|
|
35
|
+
['QuerierPoolDialect', "read the pool's own: `P['dialect']`"],
|
|
36
|
+
['QuerierPoolQuerier', "read the pool's own: `Awaited<ReturnType<P['getQuerier']>>`"],
|
|
37
|
+
[
|
|
38
|
+
'POSTGRES_WIRE_DRIVER_CAPABILITIES',
|
|
39
|
+
'pass `driverCapabilities: { nativeArrays: false, explicitJsonCast: true }` to the dialect',
|
|
40
|
+
],
|
|
41
|
+
['MysqlLikeSqlDialect', 'extend `MySqlDialect` (`uql-orm/mysql`) or `MariaDialect` (`uql-orm/mariadb`)'],
|
|
42
|
+
['D1Meta', "uql reads `D1Result['meta']`; the rest of a binding is typed by `@cloudflare/workers-types`"],
|
|
43
|
+
['D1ExecResult', 'the rest of a binding is typed by `@cloudflare/workers-types`'],
|
|
44
|
+
['createSchemaGenerator', 'use `new SqlSchemaGenerator(dialect)`, or `migrator.getSchemaGenerator()`'],
|
|
45
|
+
...['JsonMigrationStorage', 'DatabaseMigrationStorage', 'MongoMigrationStorage'].map((name) => [name, 'the migrator keeps its journal in the database itself, named by `tableName`']),
|
|
46
|
+
]);
|
|
47
|
+
/** What `uql-orm/util` exported that the root still does. */
|
|
48
|
+
const UTIL_ROOT_EXPORTS = [
|
|
49
|
+
'currentDate',
|
|
50
|
+
'currentTime',
|
|
51
|
+
'currentTimestamp',
|
|
52
|
+
'raw',
|
|
53
|
+
'refs',
|
|
54
|
+
'uuid',
|
|
55
|
+
'uuidv7',
|
|
56
|
+
'deleteFrom',
|
|
57
|
+
'insertInto',
|
|
58
|
+
'refuse',
|
|
59
|
+
'updateTable',
|
|
60
|
+
'upsertInto',
|
|
61
|
+
'withDeleted',
|
|
62
|
+
'HookContext',
|
|
63
|
+
'DefaultLogger',
|
|
64
|
+
];
|
|
65
|
+
/**
|
|
66
|
+
* Exports renamed or moved and nothing else, so the import and every use follow, the import moving to `from`
|
|
67
|
+
* where the name lives in another entry. Keyed by name, or by `entry#name` where the name stays elsewhere.
|
|
68
|
+
*/
|
|
69
|
+
const RENAMED_EXPORTS = new Map([
|
|
70
|
+
['QueryWhereMap', { to: 'QueryWhere' }],
|
|
71
|
+
['RelationKeyMap', { to: 'KeyMap' }],
|
|
72
|
+
['FilterCondition', { to: 'FilterWhere' }],
|
|
73
|
+
['PgDialect', { to: 'PostgresDialect', from: 'uql-orm/postgres' }],
|
|
74
|
+
['NeonDialect', { to: 'PostgresDialect', from: 'uql-orm/postgres' }],
|
|
75
|
+
['PgliteDialect', { to: 'PostgresDialect', from: 'uql-orm/postgres' }],
|
|
76
|
+
['CrdbQuerier', { to: 'PgQuerier', from: 'uql-orm/postgres' }],
|
|
77
|
+
['NeonQuerier', { to: 'PgQuerier', from: 'uql-orm/postgres' }],
|
|
78
|
+
['MySql2Dialect', { to: 'MySqlDialect', from: 'uql-orm/mysql' }],
|
|
79
|
+
['MongodbNativeDialect', { to: 'MongoDialect', from: 'uql-orm/mongodb' }],
|
|
80
|
+
['LibsqlQuerier', { to: 'HranaQuerier', from: 'uql-orm/sqlite' }],
|
|
81
|
+
['TursoQuerier', { to: 'HranaQuerier', from: 'uql-orm/sqlite' }],
|
|
82
|
+
['TursoLocalQuerier', { to: 'SqliteQuerier', from: 'uql-orm/sqlite' }],
|
|
83
|
+
['TursoDatabase', { to: 'SqliteDatabase', from: 'uql-orm/sqlite' }],
|
|
84
|
+
['SqlMigrationModuleOptions', { to: 'MigrationModuleOptions' }],
|
|
85
|
+
['buildSqlQuerierMigrationModule', { to: 'buildMigrationModule' }],
|
|
86
|
+
['QueryDialect', { to: 'SqlQueryDialect' }],
|
|
87
|
+
['EngineFeatures', { to: 'DialectFeatures' }],
|
|
88
|
+
['KnownMigratorDialect', { to: 'DialectName', from: 'uql-orm' }],
|
|
89
|
+
['D1Preparer', { to: 'D1Queryable' }],
|
|
90
|
+
['D1Database', { to: 'D1Queryable', from: 'uql-orm/d1' }],
|
|
91
|
+
['MongoQuerier', { to: 'MongoQuerier', from: 'uql-orm/mongodb' }],
|
|
92
|
+
['isMongoQuerier', { to: 'isMongoQuerier', from: 'uql-orm/mongodb' }],
|
|
93
|
+
['MongoSchemaIntrospector', { to: 'MongoSchemaIntrospector', from: 'uql-orm/mongodb' }],
|
|
94
|
+
['Sqlite3QuerierPool', { to: 'SqliteQuerierPool' }],
|
|
95
|
+
['Sqlite3PoolOptions', { to: 'SqlitePoolOptions' }],
|
|
96
|
+
['IMigrationBuilder', { to: 'MigrationBuilder' }],
|
|
97
|
+
['ITableBuilder', { to: 'TableBuilder' }],
|
|
98
|
+
['IAlterTableBuilder', { to: 'AlterTableBuilder' }],
|
|
99
|
+
['IColumnBuilder', { to: 'ColumnBuilder' }],
|
|
100
|
+
['IColumnFactory', { to: 'ColumnFactory' }],
|
|
101
|
+
['IForeignKeyBuilder', { to: 'ForeignKeyBuilder' }],
|
|
102
|
+
['ITableForeignKeyBuilder', { to: 'TableForeignKeyBuilder' }],
|
|
103
|
+
['UqlLockUsageError', { to: 'UqlUsageError' }],
|
|
104
|
+
['uql-orm/http#Hook', { to: 'RequestHook' }],
|
|
105
|
+
['uql-orm/http#HookContext', { to: 'RequestHookContext' }],
|
|
106
|
+
...UTIL_ROOT_EXPORTS.map((name) => [`uql-orm/util#${name}`, { to: name, from: 'uql-orm' }]),
|
|
107
|
+
]);
|
|
108
|
+
/** What the root exported that is internal now, and what to write instead. */
|
|
109
|
+
const INTERNAL_EXPORTS = new Map([
|
|
110
|
+
...['AbstractDialect', 'AbstractSqlDialect', 'AbstractQuerier', 'AbstractQuerierPool'].map((name) => [name, "extend a driver's dialect or pool from its entry instead"]),
|
|
111
|
+
...['AbstractSqlQuerier', 'AbstractSqlQuerierPool'].map((name) => [name, "extend a driver's querier or pool from its entry instead"]),
|
|
112
|
+
['SqlQueryContext', 'make one with `dialect.createContext()`, typed `QueryContext`'],
|
|
113
|
+
['dialectOptionsFrom', "pass the options to the driver's dialect"],
|
|
114
|
+
...['fieldOf', 'relationOf', 'soleIdOf', 'assertSoleId'].map((name) => [name, 'read the field off `getMeta(Entity).fields`']),
|
|
115
|
+
['namesKey', 'read the keys off `getMeta(Entity).ids`'],
|
|
116
|
+
['getEntities', 'list your entities in the config'],
|
|
117
|
+
]);
|
|
118
|
+
/** Entries renamed, or dropped for repeating the root, each to the one now exporting what it did. */
|
|
119
|
+
const MOVED_ENTRIES = new Map([
|
|
120
|
+
['uql-orm/maria', 'uql-orm/mariadb'],
|
|
121
|
+
['uql-orm/mongo', 'uql-orm/mongodb'],
|
|
122
|
+
['uql-orm/bunSql', 'uql-orm/bun-sql'],
|
|
123
|
+
['uql-orm/dialect', 'uql-orm'],
|
|
124
|
+
['uql-orm/entity', 'uql-orm'],
|
|
125
|
+
['uql-orm/namingStrategy', 'uql-orm'],
|
|
126
|
+
['uql-orm/querier', 'uql-orm'],
|
|
127
|
+
['uql-orm/type', 'uql-orm'],
|
|
128
|
+
]);
|
|
129
|
+
/**
|
|
130
|
+
* The file's named imports from `uql-orm`. `entries` adds the driver entries (`uql-orm/postgres`, ...),
|
|
131
|
+
* for what is read by name only: `idKey` written into one would land on an entry that may not export it.
|
|
132
|
+
*/
|
|
133
|
+
export function uqlImportDeclarations(source, entries = false) {
|
|
134
|
+
return source.statements.flatMap((declaration) => {
|
|
135
|
+
if (!ts.isImportDeclaration(declaration) || !ts.isStringLiteral(declaration.moduleSpecifier)) {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
const entry = declaration.moduleSpecifier.text;
|
|
139
|
+
if (entry !== 'uql-orm' && !(entries && entry.startsWith('uql-orm/'))) {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
const bindings = declaration.importClause?.namedBindings;
|
|
143
|
+
return bindings && ts.isNamedImports(bindings) ? [{ declaration, entry, elements: bindings.elements }] : [];
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function uqlImports(source, entries = false) {
|
|
147
|
+
return uqlImportDeclarations(source, entries).flatMap(({ elements }) => elements);
|
|
148
|
+
}
|
|
149
|
+
/** The specifier importing the export `name` from `uql-orm`, or with `entries` from any of its entries. */
|
|
150
|
+
export function uqlImport(source, name, entries = false) {
|
|
151
|
+
return uqlImports(source, entries).find((element) => importedName(element) === name);
|
|
152
|
+
}
|
|
153
|
+
/** The name an import specifier brings in, which is the original one where it was renamed. */
|
|
154
|
+
function importedName(element) {
|
|
155
|
+
return (element.propertyName ?? element.name).text;
|
|
156
|
+
}
|
|
157
|
+
/** Every identifier in the file bound to the same symbol as `name`, besides `name` itself. */
|
|
158
|
+
export function usesOf(source, name, checker) {
|
|
159
|
+
const symbol = checker.getSymbolAtLocation(name);
|
|
160
|
+
const uses = [];
|
|
161
|
+
const visit = (node) => {
|
|
162
|
+
if (ts.isIdentifier(node) && node !== name && node.text === name.text) {
|
|
163
|
+
if (symbol && checker.getSymbolAtLocation(node) === symbol) {
|
|
164
|
+
uses.push(node);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
ts.forEachChild(node, visit);
|
|
168
|
+
};
|
|
169
|
+
visit(source);
|
|
170
|
+
return uses;
|
|
171
|
+
}
|
|
172
|
+
/** The rename `element` still needs, none where it already names the export at its entry. */
|
|
173
|
+
function renamedTo(element, entry) {
|
|
174
|
+
const name = importedName(element);
|
|
175
|
+
const rename = RENAMED_EXPORTS.get(`${entry}#${name}`) ?? RENAMED_EXPORTS.get(name);
|
|
176
|
+
return rename?.to === name && rename.from === entry ? undefined : rename;
|
|
177
|
+
}
|
|
178
|
+
/** Names an export that no longer exists, where it is imported from the package or one of its entries. */
|
|
179
|
+
export function reportRemovedExports(ctx) {
|
|
180
|
+
for (const { declaration, entry, elements } of uqlImportDeclarations(ctx.source, true)) {
|
|
181
|
+
if (entry === 'uql-orm/util') {
|
|
182
|
+
const names = elements.filter((element) => !renamedTo(element, entry)).map(importedName);
|
|
183
|
+
if (names.length) {
|
|
184
|
+
ctx.report(declaration, `'uql-orm/util' was removed; its helpers are internal, so write your own \`${names.join('`, `')}\``);
|
|
185
|
+
}
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const root = (MOVED_ENTRIES.get(entry) ?? entry) === 'uql-orm';
|
|
189
|
+
for (const element of elements) {
|
|
190
|
+
const name = importedName(element);
|
|
191
|
+
const removed = REMOVED_EXPORTS.get(name);
|
|
192
|
+
const internal = root ? INTERNAL_EXPORTS.get(name) : undefined;
|
|
193
|
+
if (removed) {
|
|
194
|
+
ctx.report(element, `'${name}' was removed; ${removed}`);
|
|
195
|
+
}
|
|
196
|
+
else if (internal) {
|
|
197
|
+
ctx.report(element, `'${name}' is internal; ${internal}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Rewrites each import naming a {@link RENAMED_EXPORTS} export, or from a {@link MOVED_ENTRIES} entry, renaming
|
|
204
|
+
* every use: a name moving entries gets an import from its new one, and one the file already imports is dropped,
|
|
205
|
+
* as is a `dead` one. Returns the rewritten imports, which nothing else may edit.
|
|
206
|
+
*/
|
|
207
|
+
export function renameExports(dead, ctx) {
|
|
208
|
+
const imports = uqlImportDeclarations(ctx.source, true);
|
|
209
|
+
const kept = imports.flatMap(({ entry, elements }) => elements.filter((element) => !renamedTo(element, entry)));
|
|
210
|
+
const bound = new Set(kept.map((element) => element.name.text));
|
|
211
|
+
const rewritten = new Set();
|
|
212
|
+
for (const { declaration, entry: imported, elements } of imports) {
|
|
213
|
+
const entry = MOVED_ENTRIES.get(imported) ?? imported;
|
|
214
|
+
if (entry === imported && !elements.some((element) => renamedTo(element, entry))) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const byEntry = new Map([[entry, []]]);
|
|
218
|
+
for (const element of elements.filter(({ name }) => !dead.has(name.text))) {
|
|
219
|
+
const rename = renamedTo(element, entry);
|
|
220
|
+
const local = element.propertyName || !rename ? element.name.text : rename.to;
|
|
221
|
+
if (rename && !element.propertyName) {
|
|
222
|
+
ctx.edits.push(...usesOf(ctx.source, element.name, ctx.checker).map((use) => replaced(use, rename.to)));
|
|
223
|
+
}
|
|
224
|
+
if (rename && bound.has(local)) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
bound.add(local);
|
|
228
|
+
const target = rename?.from ?? entry;
|
|
229
|
+
const text = rename
|
|
230
|
+
? `${element.isTypeOnly ? 'type ' : ''}${rename.to}${element.propertyName ? ` as ${local}` : ''}`
|
|
231
|
+
: element.getText();
|
|
232
|
+
byEntry.set(target, [...(byEntry.get(target) ?? []), text]);
|
|
233
|
+
}
|
|
234
|
+
const keyword = isTypeOnly(declaration) ? 'import type' : 'import';
|
|
235
|
+
const quote = declaration.moduleSpecifier.getText()[0];
|
|
236
|
+
const statements = [...byEntry]
|
|
237
|
+
.filter(([, names]) => names.length)
|
|
238
|
+
.map(([from, names]) => `${keyword} { ${names.join(', ')} } from ${quote}${from}${quote};`);
|
|
239
|
+
ctx.edits.push(statements.length ? replaced(declaration, statements.join('\n')) : removeStatement(declaration));
|
|
240
|
+
rewritten.add(declaration);
|
|
241
|
+
}
|
|
242
|
+
return rewritten;
|
|
243
|
+
}
|
|
244
|
+
function isTypeOnly(declaration) {
|
|
245
|
+
return declaration.importClause?.phaseModifier === ts.SyntaxKind.TypeKeyword;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* The imported names the codemod removes once every use was rewritten: `Relation` unwrapped and `expr` defaults.
|
|
249
|
+
* A removed decorator keeps its import on purpose: deleting it would swap a clear error for a puzzling one.
|
|
250
|
+
*/
|
|
251
|
+
export function deadImportNames(ctx) {
|
|
252
|
+
const { source, rewritten } = ctx;
|
|
253
|
+
const uses = (name) => {
|
|
254
|
+
const element = uqlImport(source, name, true);
|
|
255
|
+
return element ? usesOf(source, element.name, ctx.checker).length : 0;
|
|
256
|
+
};
|
|
257
|
+
const relations = uses('Relation');
|
|
258
|
+
if (relations > rewritten.Relation) {
|
|
259
|
+
ctx.unresolved.push(`${source.fileName}: ${relations - rewritten.Relation} 'Relation<T>' reference(s) are somewhere this codemod ` +
|
|
260
|
+
'does not rewrite; unwrap them and drop the import by hand');
|
|
261
|
+
}
|
|
262
|
+
const names = ['Relation', 'expr'];
|
|
263
|
+
return new Set(names.filter((name) => rewritten[name] > 0 && rewritten[name] === uses(name)));
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Removes `import 'reflect-metadata'`, since the polyfill only ever fed `design:type`, and each `dead` name
|
|
267
|
+
* from the imports {@link renameExports} left as written, having left the dead names out of the rest.
|
|
268
|
+
*/
|
|
269
|
+
export function dropDeadImports(dead, rewritten, ctx) {
|
|
270
|
+
for (const statement of ctx.source.statements) {
|
|
271
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (statement.moduleSpecifier.text === 'reflect-metadata' && !statement.importClause) {
|
|
275
|
+
ctx.edits.push(removeStatement(statement));
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const named = statement.importClause?.namedBindings;
|
|
279
|
+
if (!named || !ts.isNamedImports(named) || rewritten.has(statement)) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const dropped = named.elements.filter((element) => dead.has(element.name.text));
|
|
283
|
+
if (dropped.length === named.elements.length && !statement.importClause?.name) {
|
|
284
|
+
ctx.edits.push(removeStatement(statement));
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
for (const element of dropped) {
|
|
288
|
+
ctx.edits.push(removeFromList(named.elements, element, ctx.source));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Imports what the rewrites wrote (`idKey`, `raw`) into the file's own `uql-orm` import. Reported instead
|
|
294
|
+
* where there is none: the package may be imported under a path this codemod does not recognise.
|
|
295
|
+
*/
|
|
296
|
+
export function addImports(rewritten, ctx) {
|
|
297
|
+
const { source } = ctx;
|
|
298
|
+
const imported = new Set(uqlImports(source).map(importedName));
|
|
299
|
+
const missing = [...ctx.imports].filter(([name]) => !imported.has(name));
|
|
300
|
+
if (!missing.length) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const anchor = uqlImportDeclarations(source).find(({ declaration }) => !rewritten.has(declaration));
|
|
304
|
+
if (!anchor) {
|
|
305
|
+
for (const [name, what] of missing) {
|
|
306
|
+
ctx.unresolved.push(`${source.fileName}: import '${name}' from 'uql-orm' for ${what} written here`);
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const names = missing.map(([name]) => name);
|
|
311
|
+
ctx.edits.push(isTypeOnly(anchor.declaration)
|
|
312
|
+
? inserted(anchor.declaration, `import { ${names.join(', ')} } from 'uql-orm';\n`)
|
|
313
|
+
: inserted(anchor.elements[0], names.map((name) => `${name}, `).join('')));
|
|
314
|
+
}
|
package/dist/keyMaps.d.ts
CHANGED
|
@@ -8,10 +8,6 @@ export declare function paramFor(className: string | undefined): string;
|
|
|
8
8
|
export declare function quoted(text: string): string;
|
|
9
9
|
/** `post.title`, or `post['first-name']` for a key that is no identifier. */
|
|
10
10
|
export declare function memberAccess(param: string, key: string): string;
|
|
11
|
-
/** A key's name where it is spelled out, which is every form but a computed one (`{ [k]: v }`). */
|
|
12
|
-
export declare function propertyKey(name: ts.PropertyName): string | undefined;
|
|
13
|
-
/** The initializer of the property `name` in an object literal, however its key is written. */
|
|
14
|
-
export declare function propertyValue(node: ts.ObjectLiteralExpression, name: string): ts.Expression | undefined;
|
|
15
11
|
/** `'amount'` or `['title', 'body']` -> `{ amount: true }`, the key map a statement names fields by. */
|
|
16
12
|
export declare function fieldKeysEdits(value: ts.Expression): Edits;
|
|
17
13
|
/** Whether `value` is already a function, the form every rewrite here produces. */
|
package/dist/keyMaps.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
2
|
import { inserted, replaced } from './edits.js';
|
|
3
|
+
import { identifierText, propertyValue } from './syntax.js';
|
|
3
4
|
/** A callback's parameter, named after the class it reads: `Post` -> `post`. */
|
|
4
5
|
export function paramFor(className) {
|
|
5
6
|
return className ? className[0].toLowerCase() + className.slice(1) : 'entity';
|
|
@@ -15,15 +16,6 @@ export function quoted(text) {
|
|
|
15
16
|
export function memberAccess(param, key) {
|
|
16
17
|
return isIdentifierName(key) ? `${param}.${key}` : `${param}[${quoted(key)}]`;
|
|
17
18
|
}
|
|
18
|
-
/** A key's name where it is spelled out, which is every form but a computed one (`{ [k]: v }`). */
|
|
19
|
-
export function propertyKey(name) {
|
|
20
|
-
return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : undefined;
|
|
21
|
-
}
|
|
22
|
-
/** The initializer of the property `name` in an object literal, however its key is written. */
|
|
23
|
-
export function propertyValue(node, name) {
|
|
24
|
-
const property = node.properties.find((prop) => ts.isPropertyAssignment(prop) && propertyKey(prop.name) === name);
|
|
25
|
-
return property?.initializer;
|
|
26
|
-
}
|
|
27
19
|
/** `'amount'` or `['title', 'body']` -> `{ amount: true }`, the key map a statement names fields by. */
|
|
28
20
|
export function fieldKeysEdits(value) {
|
|
29
21
|
const names = ts.isArrayLiteralExpression(value) ? value.elements : [value];
|
|
@@ -63,7 +55,7 @@ export function referencesEdits(value, owner, target) {
|
|
|
63
55
|
/** The class an `entity: () => Post` getter returns, as written. */
|
|
64
56
|
export function entityGetterTarget(relation) {
|
|
65
57
|
const getter = propertyValue(relation, 'entity');
|
|
66
|
-
return getter && ts.isArrowFunction(getter)
|
|
58
|
+
return getter && ts.isArrowFunction(getter) ? identifierText(getter.body) : undefined;
|
|
67
59
|
}
|
|
68
60
|
/** A column-list entry, its name read off the refs: an expression, or a `column` given otherwise, has none. */
|
|
69
61
|
function keyListEntryEdits(entry, param) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
/**
|
|
3
|
+
* A querier's `run(sql, values)` statement as the template its tag takes, each placeholder replaced by the value
|
|
4
|
+
* it binds, or nothing where that is not certain: the SQL is no literal, or the values no array literal of
|
|
5
|
+
* plain elements, or its placeholders cannot be told apart from the rest of the text.
|
|
6
|
+
*/
|
|
7
|
+
export declare function statementTemplate(sql: ts.Expression, values: readonly ts.Expression[]): string | undefined;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { templateOf } from './entitySql.js';
|
|
3
|
+
/**
|
|
4
|
+
* A quoted string or identifier, or a comment, skipped; a `?` or `$n` placeholder; or a character a placeholder
|
|
5
|
+
* could hide behind that this does not read: an unclosed quote or comment, a backslash, a `#`, any other `$`.
|
|
6
|
+
*/
|
|
7
|
+
const SQL_TOKEN = /('(?:[^']|'')*'|"(?:[^"]|"")*"|`[^`]*`|--[^\n]*|\/\*[\s\S]*?\*\/)|(\?)|\$([1-9]\d*)|[$\\#'"`]|\/\*/g;
|
|
8
|
+
/** `sql` split at its placeholders where they are all `?` or all `$n` and bind exactly `count` values. */
|
|
9
|
+
function placeholdersOf(sql, count) {
|
|
10
|
+
const texts = [];
|
|
11
|
+
const values = [];
|
|
12
|
+
const styles = new Set();
|
|
13
|
+
let from = 0;
|
|
14
|
+
for (const { 0: token, 1: skipped, 2: question, 3: position, index } of sql.matchAll(SQL_TOKEN)) {
|
|
15
|
+
if (skipped !== undefined) {
|
|
16
|
+
if (/\?|\$\d/.test(skipped)) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (!question && !position) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
styles.add(question ? '?' : '$');
|
|
25
|
+
values.push(position ? Number(position) - 1 : values.length);
|
|
26
|
+
texts.push(sql.slice(from, index));
|
|
27
|
+
from = index + token.length;
|
|
28
|
+
}
|
|
29
|
+
texts.push(sql.slice(from));
|
|
30
|
+
const exact = new Set(values).size === count && values.every((value) => value < count);
|
|
31
|
+
return styles.size <= 1 && exact ? { texts, values } : undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A querier's `run(sql, values)` statement as the template its tag takes, each placeholder replaced by the value
|
|
35
|
+
* it binds, or nothing where that is not certain: the SQL is no literal, or the values no array literal of
|
|
36
|
+
* plain elements, or its placeholders cannot be told apart from the rest of the text.
|
|
37
|
+
*/
|
|
38
|
+
export function statementTemplate(sql, values) {
|
|
39
|
+
if (!ts.isStringLiteral(sql) && !ts.isNoSubstitutionTemplateLiteral(sql)) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
if (!values.length) {
|
|
43
|
+
return templateOf([sql.text]);
|
|
44
|
+
}
|
|
45
|
+
const [list] = values;
|
|
46
|
+
const elements = ts.isArrayLiteralExpression(list) ? list.elements : undefined;
|
|
47
|
+
if (values.length > 1 || !elements || elements.some((it) => ts.isSpreadElement(it) || ts.isOmittedExpression(it))) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const placeholders = placeholdersOf(sql.text, elements.length);
|
|
51
|
+
return (placeholders &&
|
|
52
|
+
templateOf(placeholders.texts, placeholders.values.map((at) => elements[at].getText())));
|
|
53
|
+
}
|
package/dist/syntax.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
/** The text of `node` where it is an identifier. */
|
|
3
|
+
export declare function identifierText(node: ts.Node | undefined): string | undefined;
|
|
4
|
+
/** A key's name where it is spelled out, which is every form but a computed one (`{ [k]: v }`). */
|
|
5
|
+
export declare function propertyKey(name: ts.PropertyName): string | undefined;
|
|
6
|
+
/** A property of an object literal written with its own name: a plain or quoted key, or a shorthand. */
|
|
7
|
+
export type NamedProperty = ts.PropertyAssignment | ts.ShorthandPropertyAssignment;
|
|
8
|
+
/** The property `name` of an object literal, a shorthand included. */
|
|
9
|
+
export declare function namedProperty(node: ts.ObjectLiteralExpression, name: string): NamedProperty | undefined;
|
|
10
|
+
/** The property `name` of an object literal that is written with a value. */
|
|
11
|
+
export declare function propertyAssignment(node: ts.ObjectLiteralExpression, name: string): ts.PropertyAssignment | undefined;
|
|
12
|
+
export declare function propertyValue(node: ts.ObjectLiteralExpression, name: string): ts.Expression | undefined;
|
|
13
|
+
/** The property assignments of an object literal, or none where `node` is not one. */
|
|
14
|
+
export declare function objectProperties(node: ts.Expression | undefined): readonly ts.PropertyAssignment[];
|
|
15
|
+
/** The elements of an array literal, or none where `node` is not one. */
|
|
16
|
+
export declare function arrayElements(node: ts.Expression | undefined): readonly ts.Expression[];
|
|
17
|
+
/** `node` where it is an object literal. */
|
|
18
|
+
export declare function objectLiteral(node: ts.Expression | undefined): ts.ObjectLiteralExpression | undefined;
|
|
19
|
+
/** A node's decorators, or none where it is a kind that cannot carry them. */
|
|
20
|
+
export declare function decoratorsOf(node: ts.Node): readonly ts.Decorator[];
|
|
21
|
+
/** `Field` for `@Field` and `@Field(...)`. */
|
|
22
|
+
export declare function decoratorName(node: ts.Decorator): string | undefined;
|
package/dist/syntax.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
/** The text of `node` where it is an identifier. */
|
|
3
|
+
export function identifierText(node) {
|
|
4
|
+
return node && ts.isIdentifier(node) ? node.text : undefined;
|
|
5
|
+
}
|
|
6
|
+
/** A key's name where it is spelled out, which is every form but a computed one (`{ [k]: v }`). */
|
|
7
|
+
export function propertyKey(name) {
|
|
8
|
+
return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : undefined;
|
|
9
|
+
}
|
|
10
|
+
/** The property `name` of an object literal, a shorthand included. */
|
|
11
|
+
export function namedProperty(node, name) {
|
|
12
|
+
return node.properties.find((prop) => (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) && propertyKey(prop.name) === name);
|
|
13
|
+
}
|
|
14
|
+
/** The property `name` of an object literal that is written with a value. */
|
|
15
|
+
export function propertyAssignment(node, name) {
|
|
16
|
+
return node.properties.find((prop) => ts.isPropertyAssignment(prop) && propertyKey(prop.name) === name);
|
|
17
|
+
}
|
|
18
|
+
export function propertyValue(node, name) {
|
|
19
|
+
return propertyAssignment(node, name)?.initializer;
|
|
20
|
+
}
|
|
21
|
+
/** The property assignments of an object literal, or none where `node` is not one. */
|
|
22
|
+
export function objectProperties(node) {
|
|
23
|
+
return node && ts.isObjectLiteralExpression(node) ? node.properties.filter(ts.isPropertyAssignment) : [];
|
|
24
|
+
}
|
|
25
|
+
/** The elements of an array literal, or none where `node` is not one. */
|
|
26
|
+
export function arrayElements(node) {
|
|
27
|
+
return node && ts.isArrayLiteralExpression(node) ? node.elements : [];
|
|
28
|
+
}
|
|
29
|
+
/** `node` where it is an object literal. */
|
|
30
|
+
export function objectLiteral(node) {
|
|
31
|
+
return node && ts.isObjectLiteralExpression(node) ? node : undefined;
|
|
32
|
+
}
|
|
33
|
+
/** A node's decorators, or none where it is a kind that cannot carry them. */
|
|
34
|
+
export function decoratorsOf(node) {
|
|
35
|
+
return ts.canHaveDecorators(node) ? (ts.getDecorators(node) ?? []) : [];
|
|
36
|
+
}
|
|
37
|
+
/** `Field` for `@Field` and `@Field(...)`. */
|
|
38
|
+
export function decoratorName(node) {
|
|
39
|
+
const { expression } = node;
|
|
40
|
+
return identifierText(ts.isCallExpression(expression) ? expression.expression : expression);
|
|
41
|
+
}
|
package/dist/transform.d.ts
CHANGED
|
@@ -8,7 +8,10 @@ export type FileResult = {
|
|
|
8
8
|
/** Rewrites that are correct but worth a second look. */
|
|
9
9
|
readonly notes: readonly string[];
|
|
10
10
|
};
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Rewrites one source file for the standard decorator spec. The builder's and querier's methods are known by
|
|
13
|
+
* name, so they are read only in a file importing uql-orm, and `expr` only where it is uql's.
|
|
14
|
+
*/
|
|
12
15
|
export declare function transformFile(source: ts.SourceFile, checker: ts.TypeChecker, options?: {
|
|
13
16
|
readonly strictNullChecks: boolean;
|
|
14
17
|
}): FileResult;
|