uql-codemod 0.48.1 → 0.49.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.
Files changed (43) hide show
  1. package/README.md +27 -16
  2. package/dist/builders.d.ts +18 -0
  3. package/dist/builders.js +588 -0
  4. package/dist/classOptions.d.ts +9 -0
  5. package/dist/classOptions.js +240 -0
  6. package/dist/columnMethods.d.ts +11 -0
  7. package/dist/columnMethods.js +6 -0
  8. package/dist/columnRefs.js +3 -10
  9. package/dist/context.d.ts +18 -5
  10. package/dist/context.js +22 -4
  11. package/dist/definers.d.ts +24 -0
  12. package/dist/definers.js +34 -0
  13. package/dist/edits.d.ts +43 -11
  14. package/dist/edits.js +105 -26
  15. package/dist/entitySql.d.ts +15 -16
  16. package/dist/entitySql.js +33 -31
  17. package/dist/imports.d.ts +33 -16
  18. package/dist/imports.js +213 -99
  19. package/dist/index.d.ts +3 -3
  20. package/dist/index.js +9 -12
  21. package/dist/jsonIndex.d.ts +11 -0
  22. package/dist/jsonIndex.js +78 -0
  23. package/dist/keyMaps.d.ts +3 -1
  24. package/dist/keyMaps.js +5 -1
  25. package/dist/migrationBuilders.d.ts +13 -0
  26. package/dist/migrationBuilders.js +182 -0
  27. package/dist/relationTarget.d.ts +6 -0
  28. package/dist/relationTarget.js +21 -0
  29. package/dist/schemaEntity.d.ts +12 -0
  30. package/dist/schemaEntity.js +518 -0
  31. package/dist/schemaOptions.d.ts +6 -0
  32. package/dist/schemaOptions.js +6 -0
  33. package/dist/statementSql.d.ts +2 -1
  34. package/dist/statementSql.js +10 -4
  35. package/dist/syntax.d.ts +54 -0
  36. package/dist/syntax.js +131 -1
  37. package/dist/transform.d.ts +11 -2
  38. package/dist/transform.js +174 -165
  39. package/dist/tsconfig.d.ts +3 -9
  40. package/dist/tsconfig.js +18 -22
  41. package/package.json +1 -1
  42. package/dist/fieldType.d.ts +0 -13
  43. package/dist/fieldType.js +0 -56
package/dist/edits.js CHANGED
@@ -1,17 +1,84 @@
1
+ import ts from 'typescript';
2
+ import { lineStartOf } from './syntax.js';
3
+ /** The text of `node` as written, for a replacement to keep. */
4
+ export function original(node) {
5
+ return { start: node.getStart(), end: node.getEnd() };
6
+ }
7
+ const isInsertion = (edit) => edit.start === edit.end;
8
+ /**
9
+ * Applies edits to `source`, which is spliced rather than reprinted: a codemod that reformats everything it
10
+ * touches buries its own change in the diff. An edit inside a range another replaces is moot, unless that
11
+ * replacement keeps the stretch it sits in; edits that overlap without one holding the other are a bug.
12
+ */
13
+ export function applyEdits(source, edits) {
14
+ return editedText(source, edits).text;
15
+ }
16
+ /** `text` as a pass that changed nothing. */
17
+ export const unedited = (text) => ({ text, sourceOffset: (at) => at });
18
+ /** `second` run on what `first` wrote, each offset of its text mapped back to what `first` read. */
19
+ export function followed(first, second) {
20
+ return { text: second.text, sourceOffset: (at) => first.sourceOffset(second.sourceOffset(at)) };
21
+ }
22
+ /** `pass` edited again by `edits`, which read its text. */
23
+ function editedAgain(pass, edits) {
24
+ return followed(pass, editedText(pass.text, edits));
25
+ }
26
+ /** `pass` edited again by the edits `edits` reads off its text, parsed. */
27
+ export function reparsedAgain(fileName, pass, edits) {
28
+ return editedAgain(pass, edits(ts.createSourceFile(fileName, pass.text, ts.ScriptTarget.Latest, true)));
29
+ }
30
+ /**
31
+ * {@link applyEdits}, with where each offset of the result was in `source`, for a later pass reading the result to
32
+ * report against the file as written: an offset inside an edit's text maps to where the edit starts.
33
+ */
34
+ export function editedText(source, edits) {
35
+ const moved = [];
36
+ const ordered = edits
37
+ .map((edit, index) => ({ edit, index }))
38
+ .sort((a, b) => a.edit.start - b.edit.start ||
39
+ Number(!isInsertion(a.edit)) - Number(!isInsertion(b.edit)) ||
40
+ (isInsertion(a.edit) ? b.index - a.index : b.edit.end - a.edit.end))
41
+ .map(({ edit }) => edit);
42
+ const text = render(source, ordered, { start: 0, end: source.length }, undefined, moved);
43
+ const sourceOffset = (at) => {
44
+ const before = moved.findLast((edit) => edit.from <= at);
45
+ if (!before)
46
+ return at;
47
+ return at < before.to ? before.start : before.end + at - before.to;
48
+ };
49
+ return { text, sourceOffset };
50
+ }
1
51
  /**
2
- * Applies edits back to front, so none shifts the offsets of another, and at a shared offset the replacement
3
- * first, so an insertion there lands before it. Splicing rather than reprinting keeps the rest of the file
4
- * byte for byte: a codemod that reformats everything it touches buries its own change in the diff.
52
+ * `range` of `source` with `edits` (ordered outermost first, an insertion before a replacement at its offset, a
53
+ * later insertion before an earlier) applied. `parent` is the edit keeping `range`: an insertion at its edges
54
+ * is written beside it, so it is not written again inside.
5
55
  */
6
- export function applyEdits(text, edits) {
7
- let result = text;
8
- for (const edit of [...edits].sort((a, b) => b.start - a.start || b.end - a.end)) {
9
- result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
56
+ function render(source, edits, range, parent, moved) {
57
+ const within = (edit) => edit !== parent &&
58
+ range.start <= edit.start &&
59
+ edit.end <= range.end &&
60
+ !(parent && isInsertion(edit) && (edit.start === parent.start || edit.start === parent.end));
61
+ const inside = edits.filter(within);
62
+ let written = '';
63
+ let at = range.start;
64
+ for (const edit of inside) {
65
+ if (edit.start < at) {
66
+ if (edit.end > at) {
67
+ throw new Error(`overlapping edits at ${edit.start}`);
68
+ }
69
+ continue;
70
+ }
71
+ const parts = typeof edit.text === 'string' ? [edit.text] : edit.text;
72
+ written += source.slice(at, edit.start);
73
+ const from = written.length;
74
+ written += parts.map((part) => (typeof part === 'string' ? part : render(source, inside, part, edit))).join('');
75
+ moved?.push({ start: edit.start, end: edit.end, from, to: written.length });
76
+ at = edit.end;
10
77
  }
11
- return result;
78
+ return written + source.slice(at, range.end);
12
79
  }
13
80
  export function replaced(node, text) {
14
- return { start: node.getStart(), end: node.getEnd(), text };
81
+ return { ...original(node), text };
15
82
  }
16
83
  export function inserted(node, text) {
17
84
  return { start: node.getStart(), end: node.getStart(), text };
@@ -19,25 +86,37 @@ export function inserted(node, text) {
19
86
  export function appended(node, text) {
20
87
  return { start: node.getEnd(), end: node.getEnd(), text };
21
88
  }
89
+ /** `node`'s text with each of `uses`, the nodes inside it, written as `text`. */
90
+ export function replacingUses(node, uses, text) {
91
+ return applyEdits(node.getText(), uses.map((use) => ({ start: use.getStart() - node.getStart(), end: use.getEnd() - node.getStart(), text })));
92
+ }
22
93
  /**
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.
94
+ * Removes elements of a comma-separated list, at least one of them kept, with the separators that would dangle.
95
+ * Each removed element takes the text up to the next item; the removed run at the end takes the separator before
96
+ * it, so the ranges never overlap. `source` is passed because a tree from `ts.parseJsonText` has no `getStart()`.
26
97
  */
27
- export function removeFromList(items, item, source) {
28
- const at = items.indexOf(item);
29
- const next = items[at + 1];
30
- const previous = items[at - 1];
31
- return {
32
- start: previous && !next ? previous.getEnd() : item.getStart(source),
33
- end: next ? next.getStart(source) : item.getEnd(),
34
- text: '',
35
- };
98
+ export function removeFromList(items, removed, source) {
99
+ const kept = items.map((item) => !removed.includes(item));
100
+ const lastKept = kept.lastIndexOf(true);
101
+ const between = items
102
+ .slice(0, lastKept + 1)
103
+ .flatMap((item, at) => kept[at] ? [] : [{ start: item.getStart(source), end: items[at + 1].getStart(source), text: '' }]);
104
+ const trailing = items.slice(lastKept + 1);
105
+ if (!trailing.length) {
106
+ return between;
107
+ }
108
+ return [...between, { start: items[lastKept].getEnd(), end: items[items.length - 1].getEnd(), text: '' }];
36
109
  }
37
- /** Removes a statement together with the rest of its line, so nothing is left blank behind it. */
110
+ /**
111
+ * Removes a statement with the blanks before it on its line, the rest of its line and the blank lines after it,
112
+ * so nothing is left blank behind it.
113
+ */
38
114
  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: '' };
115
+ const text = node.getSourceFile().text;
116
+ const lineStart = lineStartOf(text, node.getStart());
117
+ const head = text.slice(lineStart, node.getStart());
118
+ const after = /[^\n]*\n?(?:[ \t]*\n)*/y;
119
+ after.lastIndex = node.getEnd();
120
+ after.exec(text);
121
+ return { start: lineStart + head.replace(/[ \t]*$/, '').length, end: after.lastIndex, text: '' };
43
122
  }
@@ -1,22 +1,21 @@
1
1
  import ts from 'typescript';
2
- import { type Edit } from './edits.js';
3
- /** `raw\`...\`` or `raw(...)`. */
4
- type RawSql = ts.TaggedTemplateExpression | ts.CallExpression;
5
- export declare function isRaw(node: ts.Node): node is RawSql;
6
- /** The expressions of a column list, written as one or returned by its callback: each entry or `column` that is `raw`. */
7
- export declare function columnExpressions(list: ts.Expression | undefined): readonly RawSql[];
8
- /** SQL text as a `raw` tagged template, escaping what would end or interpolate it. */
9
- export declare function rawTag(sql: string): string;
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;
15
- /** A partial-index `where` string as `raw`, or nothing for a template that interpolates, whose values `raw` would bind. */
16
- export declare function rawWhereEdit(where: ts.Expression): Edit | undefined;
2
+ import { type Edit, type Part, type Span } from './edits.js';
3
+ /** `sql\`...\`` or `sql(...)`, or the deprecated `raw` of either. */
4
+ type SqlCall = ts.TaggedTemplateExpression | ts.CallExpression;
5
+ export declare function isSqlCall(node: ts.Node, checker: ts.TypeChecker): node is SqlCall;
6
+ /** The expressions of a column list, written as one or returned by its callback: each entry or `column` that is `sql`. */
7
+ export declare function columnExpressions(list: ts.Expression | undefined, checker: ts.TypeChecker): readonly SqlCall[];
8
+ /** SQL text as a template tagged by `name`, escaping what would end or interpolate it. */
9
+ export declare function sqlTag(name: string, sql: string): string;
10
+ /** `text` as a template literal binding nothing: a backslash, a backtick and a `${` in it are escaped. */
11
+ export declare function templateOf(text: string): string;
12
+ /** A template literal reading `texts`, the `values` kept from the original interpolated between them. */
13
+ export declare function templateWith(texts: readonly string[], values: readonly Span[]): readonly Part[];
14
+ /** A partial-index `where` string as `sql`, or nothing for a template that interpolates, whose values `sql` would bind. */
15
+ export declare function sqlWhereEdit(where: ts.Expression, name: string): Edit | undefined;
17
16
  /**
18
17
  * The columns SQL names by hand, which a rename does not reach: each quoted identifier, and each bare word
19
18
  * naming one of `members`. What a string literal holds is left out, as is a word called as a function.
20
19
  */
21
- export declare function handNamedColumns(sql: ts.Expression, members: ReadonlySet<string>): readonly string[];
20
+ export declare function handNamedColumns(sql: ts.Expression, members: ReadonlySet<string>, checker: ts.TypeChecker): readonly string[];
22
21
  export {};
package/dist/entitySql.js CHANGED
@@ -1,60 +1,62 @@
1
1
  import ts from 'typescript';
2
2
  import { inserted, replaced } from './edits.js';
3
- import { identifierText, propertyValue } from './syntax.js';
4
- export function isRaw(node) {
3
+ import { isSqlExport, uqlExport } from './imports.js';
4
+ import { columnListEntries, propertyValue } from './syntax.js';
5
+ export function isSqlCall(node, checker) {
5
6
  const callee = ts.isTaggedTemplateExpression(node)
6
7
  ? node.tag
7
8
  : ts.isCallExpression(node)
8
9
  ? node.expression
9
10
  : undefined;
10
- const name = identifierText(callee);
11
- return name === 'raw' || name === 'sql';
12
- }
13
- /** The expressions of a column list, written as one or returned by its callback: each entry or `column` that is `raw`. */
14
- export function columnExpressions(list) {
15
- const body = list && ts.isArrowFunction(list) ? list.body : list;
16
- const entries = body && ts.isArrayLiteralExpression(body) ? body.elements : [];
17
- return entries.flatMap((entry) => {
11
+ return isSqlExport(uqlExport(callee, checker));
12
+ }
13
+ /** The expressions of a column list, written as one or returned by its callback: each entry or `column` that is `sql`. */
14
+ export function columnExpressions(list, checker) {
15
+ return columnListEntries(list).flatMap((entry) => {
18
16
  const sql = ts.isObjectLiteralExpression(entry) ? propertyValue(entry, 'column') : entry;
19
- return sql && isRaw(sql) ? [sql] : [];
17
+ return sql && isSqlCall(sql, checker) ? [sql] : [];
20
18
  });
21
19
  }
22
- /** SQL text as a `raw` tagged template, escaping what would end or interpolate it. */
23
- export function rawTag(sql) {
24
- return `raw${templateOf([sql])}`;
20
+ /** SQL text as a template tagged by `name`, escaping what would end or interpolate it. */
21
+ export function sqlTag(name, sql) {
22
+ return `${name}${templateOf(sql)}`;
25
23
  }
26
- /**
27
- * A template literal reading `texts` with the source of each of `values` interpolated between them, one fewer
28
- * than `texts`: in the texts, a backslash, a backtick and a `${` are escaped.
29
- */
30
- export function templateOf(texts, values = []) {
31
- const escaped = texts.map((text) => text.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${'));
32
- return `\`${escaped.reduce((template, text, at) => `${template}\${${values[at - 1]}}${text}`)}\``;
24
+ /** `text` as a template literal binding nothing: a backslash, a backtick and a `${` in it are escaped. */
25
+ export function templateOf(text) {
26
+ return `\`${escapedIn(text)}\``;
27
+ }
28
+ /** A template literal reading `texts`, the `values` kept from the original interpolated between them. */
29
+ export function templateWith(texts, values) {
30
+ const [first, ...rest] = texts.map(escapedIn);
31
+ return ['`', first, ...rest.flatMap((text, at) => ['${', values[at], '}', text]), '`'];
32
+ }
33
+ function escapedIn(text) {
34
+ return text.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
33
35
  }
34
- /** A partial-index `where` string as `raw`, or nothing for a template that interpolates, whose values `raw` would bind. */
35
- export function rawWhereEdit(where) {
36
+ /** A partial-index `where` string as `sql`, or nothing for a template that interpolates, whose values `sql` would bind. */
37
+ export function sqlWhereEdit(where, name) {
36
38
  if (ts.isNoSubstitutionTemplateLiteral(where)) {
37
- return inserted(where, 'raw');
39
+ return inserted(where, name);
38
40
  }
39
- return ts.isStringLiteral(where) ? replaced(where, rawTag(where.text)) : undefined;
41
+ return ts.isStringLiteral(where) ? replaced(where, sqlTag(name, where.text)) : undefined;
40
42
  }
41
43
  /**
42
44
  * The columns SQL names by hand, which a rename does not reach: each quoted identifier, and each bare word
43
45
  * naming one of `members`. What a string literal holds is left out, as is a word called as a function.
44
46
  */
45
- export function handNamedColumns(sql, members) {
46
- const code = sqlText(sql)?.replace(/'(?:[^']|'')*'/g, "''") ?? '';
47
+ export function handNamedColumns(sql, members, checker) {
48
+ const code = sqlText(sql, checker)?.replace(/'(?:[^']|'')*'/g, "''") ?? '';
47
49
  const quoted = Array.from(code.matchAll(/"[^"]+"|`[^`]+`/g), ([token]) => token.slice(1, -1));
48
50
  const bare = Array.from(code.matchAll(/\b[A-Za-z_]\w*\b(?!\s*\()/g), ([word]) => word);
49
51
  return [...new Set([...quoted, ...bare.filter((word) => members.has(word))])];
50
52
  }
51
- /** The text of a `raw` or a string, `?` standing in for each interpolation. */
52
- function sqlText(sql) {
53
+ /** The text of a `sql` or a string, `?` standing in for each interpolation. */
54
+ function sqlText(sql, checker) {
53
55
  if (!sql) {
54
56
  return undefined;
55
57
  }
56
- if (isRaw(sql)) {
57
- return sqlText(ts.isTaggedTemplateExpression(sql) ? sql.template : sql.arguments[0]);
58
+ if (isSqlCall(sql, checker)) {
59
+ return sqlText(ts.isTaggedTemplateExpression(sql) ? sql.template : sql.arguments[0], checker);
58
60
  }
59
61
  if (ts.isTemplateExpression(sql)) {
60
62
  return [sql.head, ...sql.templateSpans.map((span) => span.literal)].map((part) => part.text).join(' ? ');
package/dist/imports.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import ts from 'typescript';
2
- import type { Context } from './context.js';
2
+ import { type Context } from './context.js';
3
+ import { type Edit } from './edits.js';
4
+ /** The name `uql-orm` gave its SQL tag, and gives it now. */
5
+ export declare const LEGACY_SQL_TAG = "raw";
6
+ export declare const SQL_TAG = "sql";
3
7
  type UqlImport = {
4
8
  readonly declaration: ts.ImportDeclaration;
5
9
  readonly entry: string;
@@ -14,27 +18,40 @@ export declare function uqlImportDeclarations(source: ts.SourceFile, entries?: b
14
18
  export declare function uqlImport(source: ts.SourceFile, name: string, entries?: boolean): ts.ImportSpecifier | undefined;
15
19
  /** Every identifier in the file bound to the same symbol as `name`, besides `name` itself. */
16
20
  export declare function usesOf(source: ts.SourceFile, name: ts.Identifier, checker: ts.TypeChecker): ts.Identifier[];
21
+ /** The names among `names` the file binds or reads as something other than uql-orm's export, which a rewrite would shadow. */
22
+ export declare function foreignNames(source: ts.SourceFile, names: ReadonlySet<string>, checker: ts.TypeChecker): ReadonlySet<string>;
17
23
  /** Names an export that no longer exists, where it is imported from the package or one of its entries. */
18
24
  export declare function reportRemovedExports(ctx: Context): void;
25
+ /** The imported names of `REWRITTEN` the codemod removes, once every use was rewritten. */
26
+ export declare function deadImportNames(ctx: Context): ReadonlySet<string>;
27
+ /** The `uql-orm` export `node` names, where its symbol is an import of it or of one of its entries. */
28
+ export declare function uqlExport(node: ts.Node | undefined, checker: ts.TypeChecker): string | undefined;
29
+ /** Whether `name` is the SQL tag, by either of its names. */
30
+ export declare function isSqlExport(name: string | undefined): boolean;
31
+ /** The name the file calls the SQL tag by once `raw` is renamed: the alias it imports it as, else `sql`. */
32
+ export declare function sqlName(source: ts.SourceFile): string;
19
33
  /**
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.
34
+ * The first identifier of the file already naming something a rewrite writes: a name it imports (`sql`, `refs`,
35
+ * `idKey`) or a rename brings in. The write would shadow it or be shadowed, so the file is left to its owner.
28
36
  */
29
- export declare function deadImportNames(ctx: Context): ReadonlySet<string>;
37
+ export declare function clashingName(ctx: Context): ts.Identifier | undefined;
30
38
  /**
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.
39
+ * Rewrites the file's imports in one pass: a name {@link RENAMED_EXPORTS} renames or a {@link MOVED_ENTRIES} entry
40
+ * moves is imported again where it lives now, every use renamed, and one already imported or `dead` is dropped.
41
+ * The names the rewrites wrote join the first `uql-orm` import, or are reported when there is none.
33
42
  */
34
- export declare function dropDeadImports(dead: ReadonlySet<string>, rewritten: ReadonlySet<ts.ImportDeclaration>, ctx: Context): void;
43
+ export declare function rewriteImports(dead: ReadonlySet<string>, ctx: Context): void;
44
+ /** An import of `names` from `from`, quoted as the file quotes its imports. */
45
+ export declare const importText: (keyword: string, names: readonly string[], from: string, quote?: string) => string;
35
46
  /**
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.
47
+ * The import without `dropped` of its `elements`: the whole statement where nothing else is left of it, and the
48
+ * braces with them where a default import stays.
38
49
  */
39
- export declare function addImports(rewritten: ReadonlySet<ts.ImportDeclaration>, ctx: Context): void;
50
+ export declare function importRemovalEdits(declaration: ts.ImportDeclaration, elements: readonly ts.ImportSpecifier[], dropped: readonly ts.ImportSpecifier[]): readonly Edit[];
51
+ /** Every name the file spells outside its imports, which an import it binds is still used under if listed here. */
52
+ export declare function spelledNames(source: ts.SourceFile): ReadonlySet<string>;
53
+ /** The import names in `candidates` that the file no longer spells. */
54
+ export declare function deadImports(elements: readonly ts.ImportSpecifier[], candidates: ReadonlySet<string | undefined>, spelled: ReadonlySet<string>): readonly ts.ImportSpecifier[];
55
+ /** The edits dropping each `uql-orm` import name in `candidates` that the file no longer spells. */
56
+ export declare function unusedImportEdits(source: ts.SourceFile, candidates: ReadonlySet<string>): readonly Edit[];
40
57
  export {};