uql-codemod 0.47.0 → 0.48.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/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 +2 -2
package/dist/cli.d.ts
CHANGED
|
@@ -7,11 +7,8 @@ export type ParsedArgs = {
|
|
|
7
7
|
readonly errors: readonly string[];
|
|
8
8
|
};
|
|
9
9
|
/**
|
|
10
|
-
* Reads the command line, rejecting anything it does not recognise
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* rewrite the project for real, which is the one mistake this tool must not let someone make quietly.
|
|
14
|
-
* Only the `--flag=value` form is accepted, so a stray `--project foo` cannot silently fall back to the
|
|
15
|
-
* default config either.
|
|
10
|
+
* Reads the command line, rejecting anything it does not recognise: a misspelled `--dry-run` would otherwise
|
|
11
|
+
* rewrite the project for real. Only `--flag=value` is accepted, so a stray `--project foo` cannot fall back
|
|
12
|
+
* to the default config either.
|
|
16
13
|
*/
|
|
17
14
|
export declare function parseArgs(argv: readonly string[]): ParsedArgs;
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
const VALUE_FLAGS = new Set(['project', 'include']);
|
|
2
2
|
/**
|
|
3
|
-
* Reads the command line, rejecting anything it does not recognise
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* rewrite the project for real, which is the one mistake this tool must not let someone make quietly.
|
|
7
|
-
* Only the `--flag=value` form is accepted, so a stray `--project foo` cannot silently fall back to the
|
|
8
|
-
* default config either.
|
|
3
|
+
* Reads the command line, rejecting anything it does not recognise: a misspelled `--dry-run` would otherwise
|
|
4
|
+
* rewrite the project for real. Only `--flag=value` is accepted, so a stray `--project foo` cannot fall back
|
|
5
|
+
* to the default config either.
|
|
9
6
|
*/
|
|
10
7
|
export function parseArgs(argv) {
|
|
11
8
|
const errors = [];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Context } from './context.js';
|
|
2
|
+
/**
|
|
3
|
+
* Rewrites each `col('x')` into the ref it names - `product.x` in a `computed` callback, `refs(Item).x` in a
|
|
4
|
+
* statement - where the entity its SQL renders against, and a member named `x`, can be told from here. Every
|
|
5
|
+
* other use is reported. Returns `col` as a dead import once no use is left.
|
|
6
|
+
*/
|
|
7
|
+
export declare function rewriteColumnRefs(ctx: Context): readonly string[];
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { constructedType, memberNames, ownerOf, propertyNames } from './context.js';
|
|
3
|
+
import { inserted, replaced } from './edits.js';
|
|
4
|
+
import { isRaw } from './entitySql.js';
|
|
5
|
+
import { uqlImport, usesOf } from './imports.js';
|
|
6
|
+
import { memberAccess } from './keyMaps.js';
|
|
7
|
+
import { identifierText, propertyKey, propertyValue } from './syntax.js';
|
|
8
|
+
/**
|
|
9
|
+
* Rewrites each `col('x')` into the ref it names - `product.x` in a `computed` callback, `refs(Item).x` in a
|
|
10
|
+
* statement - where the entity its SQL renders against, and a member named `x`, can be told from here. Every
|
|
11
|
+
* other use is reported. Returns `col` as a dead import once no use is left.
|
|
12
|
+
*/
|
|
13
|
+
export function rewriteColumnRefs(ctx) {
|
|
14
|
+
const element = uqlImport(ctx.source, 'col');
|
|
15
|
+
if (!element) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
const computed = new Map();
|
|
19
|
+
const unread = [];
|
|
20
|
+
for (const use of usesOf(ctx.source, element.name, ctx.checker)) {
|
|
21
|
+
const call = ts.isCallExpression(use.parent) && use.parent.expression === use ? use.parent : undefined;
|
|
22
|
+
const sql = call && ts.findAncestor(call, isRaw);
|
|
23
|
+
const option = sql?.parent;
|
|
24
|
+
const owner = option && ts.isPropertyAssignment(option) && isComputedOption(option) ? computedOwner(option) : undefined;
|
|
25
|
+
const edit = call && !owner ? statementRefEdit(call, ctx) : undefined;
|
|
26
|
+
if (call && sql && ts.isTaggedTemplateExpression(sql) && owner) {
|
|
27
|
+
computed.set(sql, { owner, calls: [...(computed.get(sql)?.calls ?? []), call] });
|
|
28
|
+
}
|
|
29
|
+
else if (edit) {
|
|
30
|
+
ctx.edits.push(edit);
|
|
31
|
+
ctx.imports.set('refs', 'the refs(...)');
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
unread.push(use);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (const [sql, { owner, calls }] of computed) {
|
|
38
|
+
const edits = computedRefEdits(sql, calls, owner, ctx);
|
|
39
|
+
if (edits) {
|
|
40
|
+
ctx.edits.push(...edits);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
unread.push(...calls);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (const node of unread.sort((a, b) => a.getStart() - b.getStart())) {
|
|
47
|
+
ctx.report(node, "'col' was removed; read the column off refs(Entity), which the codemod could not tell here");
|
|
48
|
+
}
|
|
49
|
+
return unread.length ? [] : ['col'];
|
|
50
|
+
}
|
|
51
|
+
function isComputedOption(option) {
|
|
52
|
+
const key = propertyKey(option.name);
|
|
53
|
+
return key === 'computed' || key === 'virtual';
|
|
54
|
+
}
|
|
55
|
+
/** The entity a field's `computed` SQL is declared on: the decorated property's class, or `defineEntity`'s first argument. */
|
|
56
|
+
function computedOwner(option) {
|
|
57
|
+
const holder = ts.findAncestor(option, (node) => ts.isPropertyDeclaration(node) ||
|
|
58
|
+
(ts.isCallExpression(node) && identifierText(node.expression) === 'defineEntity'));
|
|
59
|
+
if (holder && ts.isPropertyDeclaration(holder)) {
|
|
60
|
+
return ownerOf(holder.parent);
|
|
61
|
+
}
|
|
62
|
+
const [entity] = holder?.arguments ?? [];
|
|
63
|
+
return entity && ownerOf(entity);
|
|
64
|
+
}
|
|
65
|
+
/** `(product) => ` before a `computed` raw, and `product.x` for each `col('x')` in it, where every one names a member. */
|
|
66
|
+
function computedRefEdits(sql, calls, owner, ctx) {
|
|
67
|
+
const members = memberNames(owner.entity, ctx.checker);
|
|
68
|
+
const names = calls.map((call) => columnMember(call, members));
|
|
69
|
+
if (mentions(sql, owner.param) || !names.every((name) => name !== undefined)) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
return [
|
|
73
|
+
inserted(sql, `(${owner.param}) => `),
|
|
74
|
+
...calls.map((call, at) => replaced(call, memberAccess(owner.param, names[at]))),
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
/** `refs(Item).x` for a `col('x')` in a statement, where the entity its SQL renders against can be told. */
|
|
78
|
+
function statementRefEdit(call, ctx) {
|
|
79
|
+
const entity = statementEntity(call, ctx.checker);
|
|
80
|
+
const name = entity && columnMember(call, propertyNames(entity.type));
|
|
81
|
+
return entity && name !== undefined ? replaced(call, memberAccess(`refs(${entity.name})`, name)) : undefined;
|
|
82
|
+
}
|
|
83
|
+
/** The member a `col('x')` names, where its one argument is a literal naming one of `members`. */
|
|
84
|
+
function columnMember(call, members) {
|
|
85
|
+
const [name] = call.arguments;
|
|
86
|
+
return call.arguments.length === 1 && ts.isStringLiteralLike(name) && members.has(name.text) ? name.text : undefined;
|
|
87
|
+
}
|
|
88
|
+
/** Whether `node` reads a binding named `name`, which a callback parameter of that name would shadow. */
|
|
89
|
+
function mentions(node, name) {
|
|
90
|
+
if (ts.isIdentifier(node)) {
|
|
91
|
+
return node.text === name && !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node);
|
|
92
|
+
}
|
|
93
|
+
return ts.forEachChild(node, (child) => mentions(child, name) || undefined) ?? false;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The entity a statement's SQL renders against at `node`: the one its query names, or a relation a `$populate`
|
|
97
|
+
* reads there. Nothing where a function or a relation filter in between opens a scope of its own.
|
|
98
|
+
*/
|
|
99
|
+
function statementEntity(node, checker) {
|
|
100
|
+
const path = [];
|
|
101
|
+
for (let at = node; at.parent; at = at.parent) {
|
|
102
|
+
const { parent } = at;
|
|
103
|
+
if (ts.isFunctionLike(parent)) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
if (ts.isPropertyAssignment(parent)) {
|
|
107
|
+
path.unshift(propertyKey(parent.name) ?? '');
|
|
108
|
+
}
|
|
109
|
+
const queried = ts.isCallExpression(parent) && parent.arguments.some((argument) => argument === at);
|
|
110
|
+
const entity = queried ? queriedEntity(parent, checker) : undefined;
|
|
111
|
+
if (entity) {
|
|
112
|
+
return entityAlong(entity, path, node, checker);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
/** The entity a call queries: its first argument's class, or the `$entity` its query names. */
|
|
118
|
+
function queriedEntity(call, checker) {
|
|
119
|
+
const [first] = call.arguments;
|
|
120
|
+
const entity = first && ts.isObjectLiteralExpression(first) ? propertyValue(first, '$entity') : first;
|
|
121
|
+
const instance = entity && constructedType(checker.getTypeAtLocation(entity));
|
|
122
|
+
return entity && ts.isIdentifier(entity) && instance ? { name: entity.text, type: instance } : undefined;
|
|
123
|
+
}
|
|
124
|
+
/** `entity` followed along `path`: into each relation a `$populate` names, and nowhere a relation filter opens. */
|
|
125
|
+
function entityAlong(entity, path, node, checker) {
|
|
126
|
+
let current = entity;
|
|
127
|
+
for (const [at, key] of path.entries()) {
|
|
128
|
+
const property = current.type.getProperty(key);
|
|
129
|
+
const related = property && relatedClass(property, node, checker);
|
|
130
|
+
if (!related) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const inScope = checker.getSymbolsInScope(node, ts.SymbolFlags.Class).some(({ name }) => name === related.name);
|
|
134
|
+
if (path[at - 1] !== '$populate' || !inScope) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
current = { name: related.name, type: checker.getDeclaredTypeOfSymbol(related) };
|
|
138
|
+
}
|
|
139
|
+
return current;
|
|
140
|
+
}
|
|
141
|
+
/** The class a relation property points at, one or many, or nothing where the property holds no class. */
|
|
142
|
+
function relatedClass(property, node, checker) {
|
|
143
|
+
const type = checker.getNonNullableType(checker.getTypeOfSymbolAtLocation(property, node));
|
|
144
|
+
const symbol = (checker.getIndexTypeOfType(type, ts.IndexKind.Number) ?? type).getSymbol();
|
|
145
|
+
return symbol && symbol.flags & ts.SymbolFlags.Class ? symbol : undefined;
|
|
146
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import type { Edit } from './edits.js';
|
|
3
|
+
/** What a rewrite of one file reads and writes. */
|
|
4
|
+
export type Context = {
|
|
5
|
+
readonly source: ts.SourceFile;
|
|
6
|
+
readonly checker: ts.TypeChecker;
|
|
7
|
+
/** Whether the project checks nulls; without it a `| null` written on a property changes nothing. */
|
|
8
|
+
readonly strictNullChecks: boolean;
|
|
9
|
+
readonly edits: Edit[];
|
|
10
|
+
readonly unresolved: string[];
|
|
11
|
+
readonly notes: string[];
|
|
12
|
+
/** Names written that the file has to import from `uql-orm`, each with what it was written for. */
|
|
13
|
+
readonly imports: Map<string, string>;
|
|
14
|
+
/** Uses rewritten of each name whose import goes once every use was: `Relation<T>` unwrapped, `expr` defaults. */
|
|
15
|
+
readonly rewritten: {
|
|
16
|
+
Relation: number;
|
|
17
|
+
expr: number;
|
|
18
|
+
};
|
|
19
|
+
/** Records what needs a human, at `node`. */
|
|
20
|
+
readonly report: (node: ts.Node, message: string) => void;
|
|
21
|
+
/** Records a rewrite that is correct but worth a second look, at `node`. */
|
|
22
|
+
readonly note: (node: ts.Node, message: string) => void;
|
|
23
|
+
};
|
|
24
|
+
export declare function createContext(source: ts.SourceFile, checker: ts.TypeChecker, strictNullChecks: boolean): Context;
|
|
25
|
+
/** The entity a definition describes: the parameter its callbacks are named after, and the node naming it. */
|
|
26
|
+
export type Owner = {
|
|
27
|
+
readonly param: string;
|
|
28
|
+
readonly entity: ts.Node;
|
|
29
|
+
};
|
|
30
|
+
/** The owner a class, or an expression naming one, defines. */
|
|
31
|
+
export declare function ownerOf(entity: ts.Node): Owner;
|
|
32
|
+
/** The instance type a class type constructs, if it constructs one. */
|
|
33
|
+
export declare function constructedType(type: ts.Type): ts.Type | undefined;
|
|
34
|
+
/** The instance type of the entity `node` declares or names. */
|
|
35
|
+
export declare function instanceTypeOf(node: ts.Node, checker: ts.TypeChecker): ts.Type;
|
|
36
|
+
export declare function propertyNames(type: ts.Type): ReadonlySet<string>;
|
|
37
|
+
/** The names of the members of the entity `node` declares or names. */
|
|
38
|
+
export declare function memberNames(node: ts.Node, checker: ts.TypeChecker): ReadonlySet<string>;
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { paramFor } from './keyMaps.js';
|
|
3
|
+
import { identifierText } from './syntax.js';
|
|
4
|
+
export function createContext(source, checker, strictNullChecks) {
|
|
5
|
+
const unresolved = [];
|
|
6
|
+
const notes = [];
|
|
7
|
+
const at = (node) => `${source.fileName}:${source.getLineAndCharacterOfPosition(node.getStart()).line + 1}`;
|
|
8
|
+
return {
|
|
9
|
+
source,
|
|
10
|
+
checker,
|
|
11
|
+
strictNullChecks,
|
|
12
|
+
edits: [],
|
|
13
|
+
unresolved,
|
|
14
|
+
notes,
|
|
15
|
+
imports: new Map(),
|
|
16
|
+
rewritten: { Relation: 0, expr: 0 },
|
|
17
|
+
report: (node, message) => {
|
|
18
|
+
unresolved.push(`${at(node)}: ${message}`);
|
|
19
|
+
},
|
|
20
|
+
note: (node, message) => {
|
|
21
|
+
notes.push(`${at(node)}: ${message}`);
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** The owner a class, or an expression naming one, defines. */
|
|
26
|
+
export function ownerOf(entity) {
|
|
27
|
+
return { param: paramFor(identifierText(ts.isClassLike(entity) ? entity.name : entity)), entity };
|
|
28
|
+
}
|
|
29
|
+
/** The instance type a class type constructs, if it constructs one. */
|
|
30
|
+
export function constructedType(type) {
|
|
31
|
+
return type.getConstructSignatures()[0]?.getReturnType();
|
|
32
|
+
}
|
|
33
|
+
/** The instance type of the entity `node` declares or names. */
|
|
34
|
+
export function instanceTypeOf(node, checker) {
|
|
35
|
+
const type = checker.getTypeAtLocation(node);
|
|
36
|
+
return constructedType(type) ?? type;
|
|
37
|
+
}
|
|
38
|
+
export function propertyNames(type) {
|
|
39
|
+
return new Set(type.getProperties().map(({ name }) => name));
|
|
40
|
+
}
|
|
41
|
+
/** The names of the members of the entity `node` declares or names. */
|
|
42
|
+
export function memberNames(node, checker) {
|
|
43
|
+
return propertyNames(instanceTypeOf(node, checker));
|
|
44
|
+
}
|
package/dist/edits.d.ts
CHANGED
|
@@ -15,11 +15,10 @@ export declare function replaced(node: ts.Node, text: string): Edit;
|
|
|
15
15
|
export declare function inserted(node: ts.Node, text: string): Edit;
|
|
16
16
|
export declare function appended(node: ts.Node, text: string): Edit;
|
|
17
17
|
/**
|
|
18
|
-
* Removes one element of a comma-separated list
|
|
19
|
-
* the
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* The source file is passed explicitly because a tree from `ts.parseJsonText` carries no
|
|
23
|
-
* back-reference, so `getStart()` cannot find one on its own.
|
|
18
|
+
* Removes one element of a comma-separated list with the separator that would dangle: the comma before the
|
|
19
|
+
* last element, the one after any other. `source` is passed because a tree from `ts.parseJsonText` carries
|
|
20
|
+
* no back-reference for `getStart()` to find.
|
|
24
21
|
*/
|
|
25
22
|
export declare function removeFromList(items: readonly ts.Node[], item: ts.Node, source: ts.SourceFile): Edit;
|
|
23
|
+
/** Removes a statement together with the rest of its line, so nothing is left blank behind it. */
|
|
24
|
+
export declare function removeStatement(node: ts.Node): Edit;
|
package/dist/edits.js
CHANGED
|
@@ -20,12 +20,9 @@ export function appended(node, text) {
|
|
|
20
20
|
return { start: node.getEnd(), end: node.getEnd(), text };
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
* Removes one element of a comma-separated list
|
|
24
|
-
* the
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* The source file is passed explicitly because a tree from `ts.parseJsonText` carries no
|
|
28
|
-
* back-reference, so `getStart()` cannot find one on its own.
|
|
23
|
+
* Removes one element of a comma-separated list with the separator that would dangle: the comma before the
|
|
24
|
+
* last element, the one after any other. `source` is passed because a tree from `ts.parseJsonText` carries
|
|
25
|
+
* no back-reference for `getStart()` to find.
|
|
29
26
|
*/
|
|
30
27
|
export function removeFromList(items, item, source) {
|
|
31
28
|
const at = items.indexOf(item);
|
|
@@ -37,3 +34,10 @@ export function removeFromList(items, item, source) {
|
|
|
37
34
|
text: '',
|
|
38
35
|
};
|
|
39
36
|
}
|
|
37
|
+
/** Removes a statement together with the rest of its line, so nothing is left blank behind it. */
|
|
38
|
+
export function removeStatement(node) {
|
|
39
|
+
const source = node.getSourceFile();
|
|
40
|
+
const line = source.getLineAndCharacterOfPosition(node.getEnd()).line;
|
|
41
|
+
const nextLineStart = source.getLineStarts()[line + 1];
|
|
42
|
+
return { start: node.getStart(), end: nextLineStart ?? node.getEnd(), text: '' };
|
|
43
|
+
}
|
package/dist/entitySql.d.ts
CHANGED
|
@@ -7,8 +7,11 @@ 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
|
-
/**
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* A template literal reading `texts` with the source of each of `values` interpolated between them, one fewer
|
|
12
|
+
* than `texts`: in the texts, a backslash, a backtick and a `${` are escaped.
|
|
13
|
+
*/
|
|
14
|
+
export declare function templateOf(texts: readonly string[], values?: readonly string[]): string;
|
|
12
15
|
/** A partial-index `where` string as `raw`, or nothing for a template that interpolates, whose values `raw` would bind. */
|
|
13
16
|
export declare function rawWhereEdit(where: ts.Expression): Edit | undefined;
|
|
14
17
|
/**
|
package/dist/entitySql.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
2
|
import { inserted, replaced } from './edits.js';
|
|
3
|
-
import { propertyValue } from './
|
|
3
|
+
import { identifierText, propertyValue } from './syntax.js';
|
|
4
4
|
export function isRaw(node) {
|
|
5
5
|
const callee = ts.isTaggedTemplateExpression(node)
|
|
6
6
|
? node.tag
|
|
7
7
|
: ts.isCallExpression(node)
|
|
8
8
|
? node.expression
|
|
9
9
|
: undefined;
|
|
10
|
-
return
|
|
10
|
+
return identifierText(callee) === 'raw';
|
|
11
11
|
}
|
|
12
12
|
/** The expressions of a column list, written as one or returned by its callback: each entry or `column` that is `raw`. */
|
|
13
13
|
export function columnExpressions(list) {
|
|
@@ -20,11 +20,15 @@ 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${templateOf(sql)}`;
|
|
23
|
+
return `raw${templateOf([sql])}`;
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
/**
|
|
26
|
+
* A template literal reading `texts` with the source of each of `values` interpolated between them, one fewer
|
|
27
|
+
* than `texts`: in the texts, a backslash, a backtick and a `${` are escaped.
|
|
28
|
+
*/
|
|
29
|
+
export function templateOf(texts, values = []) {
|
|
30
|
+
const escaped = texts.map((text) => text.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${'));
|
|
31
|
+
return `\`${escaped.reduce((template, text, at) => `${template}\${${values[at - 1]}}${text}`)}\``;
|
|
28
32
|
}
|
|
29
33
|
/** A partial-index `where` string as `raw`, or nothing for a template that interpolates, whose values `raw` would bind. */
|
|
30
34
|
export function rawWhereEdit(where) {
|
package/dist/fieldType.d.ts
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
|
-
/** A string type carrying more than `string`, e.g. `` type UUID = `${string}-${string}` ``. */
|
|
3
2
|
export declare function isBrandedString(type: ts.Type): boolean;
|
|
4
3
|
/**
|
|
5
|
-
* The `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Mirrors `TypeFor` in `uql-orm`, in the direction the codemod needs: from the property's TypeScript type
|
|
9
|
-
* to the `type` option that reflection used to supply. Only the cases reflection could actually produce
|
|
10
|
-
* are handled. A `Json<T>` or vector property already had to declare its `type` explicitly, because
|
|
11
|
-
* `design:type` reported the useless `Object`/`Array` for them, so any such property reaching here is
|
|
12
|
-
* genuinely ambiguous and is reported rather than guessed.
|
|
4
|
+
* The `type` option reflection supplied for a property of `type`, mirroring `TypeFor` in `uql-orm`, or nothing
|
|
5
|
+
* for a shape a human decides. Only what reflection could produce is read: a `Json<T>` or vector property got
|
|
6
|
+
* `Object`/`Array` from it, so it already declared its `type`.
|
|
13
7
|
*/
|
|
14
8
|
export declare function fieldTypeFor(type: ts.Type): string | undefined;
|
|
15
9
|
/**
|
package/dist/fieldType.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
|
-
/**
|
|
2
|
+
/** The string types carrying more than `string`, e.g. `` type UUID = `${string}-${string}` ``, still string columns. */
|
|
3
|
+
const BRANDED_STRING = ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping;
|
|
3
4
|
export function isBrandedString(type) {
|
|
4
|
-
|
|
5
|
-
return type.isUnion() ? type.types.some((t) => !!(t.getFlags() & branded)) : !!(type.getFlags() & branded);
|
|
5
|
+
return (type.isUnion() ? type.types : [type]).some((t) => !!(t.getFlags() & BRANDED_STRING));
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
8
|
* Reads a union by requiring its arms to agree, which is what an `'a' | 'b'` enum or a `T | null` looks
|
|
@@ -14,24 +14,16 @@ function foldUnion(type, read) {
|
|
|
14
14
|
return readings.size === 1 ? [...readings][0] : undefined;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* The `
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* Mirrors `TypeFor` in `uql-orm`, in the direction the codemod needs: from the property's TypeScript type
|
|
21
|
-
* to the `type` option that reflection used to supply. Only the cases reflection could actually produce
|
|
22
|
-
* are handled. A `Json<T>` or vector property already had to declare its `type` explicitly, because
|
|
23
|
-
* `design:type` reported the useless `Object`/`Array` for them, so any such property reaching here is
|
|
24
|
-
* genuinely ambiguous and is reported rather than guessed.
|
|
17
|
+
* The `type` option reflection supplied for a property of `type`, mirroring `TypeFor` in `uql-orm`, or nothing
|
|
18
|
+
* for a shape a human decides. Only what reflection could produce is read: a `Json<T>` or vector property got
|
|
19
|
+
* `Object`/`Array` from it, so it already declared its `type`.
|
|
25
20
|
*/
|
|
26
21
|
export function fieldTypeFor(type) {
|
|
27
22
|
const flags = type.getFlags();
|
|
28
23
|
if (type.isUnion()) {
|
|
29
24
|
return foldUnion(type, fieldTypeFor);
|
|
30
25
|
}
|
|
31
|
-
|
|
32
|
-
// `type UUID = `${string}-${string}-${string}-${string}-${string}``, which are still string columns.
|
|
33
|
-
const stringLike = ts.TypeFlags.String | ts.TypeFlags.StringLiteral | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping;
|
|
34
|
-
if (flags & stringLike)
|
|
26
|
+
if (flags & (ts.TypeFlags.String | ts.TypeFlags.StringLiteral | BRANDED_STRING))
|
|
35
27
|
return 'String';
|
|
36
28
|
if (flags & (ts.TypeFlags.Number | ts.TypeFlags.NumberLiteral))
|
|
37
29
|
return 'Number';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import type { Context } from './context.js';
|
|
3
|
+
type UqlImport = {
|
|
4
|
+
readonly declaration: ts.ImportDeclaration;
|
|
5
|
+
readonly entry: string;
|
|
6
|
+
readonly elements: readonly ts.ImportSpecifier[];
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* The file's named imports from `uql-orm`. `entries` adds the driver entries (`uql-orm/postgres`, ...),
|
|
10
|
+
* for what is read by name only: `idKey` written into one would land on an entry that may not export it.
|
|
11
|
+
*/
|
|
12
|
+
export declare function uqlImportDeclarations(source: ts.SourceFile, entries?: boolean): readonly UqlImport[];
|
|
13
|
+
/** The specifier importing the export `name` from `uql-orm`, or with `entries` from any of its entries. */
|
|
14
|
+
export declare function uqlImport(source: ts.SourceFile, name: string, entries?: boolean): ts.ImportSpecifier | undefined;
|
|
15
|
+
/** Every identifier in the file bound to the same symbol as `name`, besides `name` itself. */
|
|
16
|
+
export declare function usesOf(source: ts.SourceFile, name: ts.Identifier, checker: ts.TypeChecker): ts.Identifier[];
|
|
17
|
+
/** Names an export that no longer exists, where it is imported from the package or one of its entries. */
|
|
18
|
+
export declare function reportRemovedExports(ctx: Context): void;
|
|
19
|
+
/**
|
|
20
|
+
* Rewrites each import naming a {@link RENAMED_EXPORTS} export, or from a {@link MOVED_ENTRIES} entry, renaming
|
|
21
|
+
* every use: a name moving entries gets an import from its new one, and one the file already imports is dropped,
|
|
22
|
+
* as is a `dead` one. Returns the rewritten imports, which nothing else may edit.
|
|
23
|
+
*/
|
|
24
|
+
export declare function renameExports(dead: ReadonlySet<string>, ctx: Context): ReadonlySet<ts.ImportDeclaration>;
|
|
25
|
+
/**
|
|
26
|
+
* The imported names the codemod removes once every use was rewritten: `Relation` unwrapped and `expr` defaults.
|
|
27
|
+
* A removed decorator keeps its import on purpose: deleting it would swap a clear error for a puzzling one.
|
|
28
|
+
*/
|
|
29
|
+
export declare function deadImportNames(ctx: Context): ReadonlySet<string>;
|
|
30
|
+
/**
|
|
31
|
+
* Removes `import 'reflect-metadata'`, since the polyfill only ever fed `design:type`, and each `dead` name
|
|
32
|
+
* from the imports {@link renameExports} left as written, having left the dead names out of the rest.
|
|
33
|
+
*/
|
|
34
|
+
export declare function dropDeadImports(dead: ReadonlySet<string>, rewritten: ReadonlySet<ts.ImportDeclaration>, ctx: Context): void;
|
|
35
|
+
/**
|
|
36
|
+
* Imports what the rewrites wrote (`idKey`, `raw`) into the file's own `uql-orm` import. Reported instead
|
|
37
|
+
* where there is none: the package may be imported under a path this codemod does not recognise.
|
|
38
|
+
*/
|
|
39
|
+
export declare function addImports(rewritten: ReadonlySet<ts.ImportDeclaration>, ctx: Context): void;
|
|
40
|
+
export {};
|