@tailor-platform/sdk-codemod 0.7.0 → 0.8.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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @tailor-platform/sdk-codemod
|
|
2
2
|
|
|
3
|
+
## 0.8.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#2153](https://github.com/tailor-platform/sdk/pull/2153) [`07ac5d0`](https://github.com/tailor-platform/sdk/commit/07ac5d019a14143438bf1072fe2bf7a7e9f6980c) Thanks [@renovate](https://github.com/apps/renovate)! - fix(deps): update dependency @inquirer/prompts to v8.6.0
|
|
8
|
+
|
|
9
|
+
## 0.8.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [#2136](https://github.com/tailor-platform/sdk/pull/2136) [`6fba096`](https://github.com/tailor-platform/sdk/commit/6fba09676fc20e08e3325c26c0e72dc9ed4fd8f6) Thanks [@toiroakr](https://github.com/toiroakr)! - `.relation()`'s `toward.type` option is renamed to `toward.table`, since it names a target table rather than a TypeScript/GraphQL type — matching the `db.type()` → `db.table()` rename. The old spelling keeps working as a deprecated alias until v3; `tailor upgrade` offers the `v3/relation-toward-table` codemod to rewrite `toward: { type: ... }` to `toward: { table: ... }` across TypeScript/JavaScript sources. The relation's own `type` (its cardinality, e.g. `"n-1"`) is unchanged.
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- [#2135](https://github.com/tailor-platform/sdk/pull/2135) [`5b7b676`](https://github.com/tailor-platform/sdk/commit/5b7b676740350dfe35ae479aa73da1f67c4d4f2f) Thanks [@renovate](https://github.com/apps/renovate)! - fix(deps): update dependency politty to v0.11.9
|
|
18
|
+
|
|
3
19
|
## 0.7.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { l as stringValue } from "../../../ast-grep-helpers-CXtWn3RB.js";
|
|
2
|
+
import { Lang, parse } from "@ast-grep/napi";
|
|
3
|
+
//#region codemods/v3/relation-toward-table/scripts/transform.ts
|
|
4
|
+
const LEGACY_KEY = "type";
|
|
5
|
+
const NEW_KEY = "table";
|
|
6
|
+
function sourceLang(filePath, source) {
|
|
7
|
+
const lowerPath = filePath.toLowerCase();
|
|
8
|
+
if (/\.(?:ts|mts|cts)$/u.test(lowerPath)) return Lang.TypeScript;
|
|
9
|
+
if (/\.(?:tsx|jsx|js)$/u.test(lowerPath)) return Lang.Tsx;
|
|
10
|
+
return source.includes("</") ? Lang.Tsx : Lang.TypeScript;
|
|
11
|
+
}
|
|
12
|
+
function relationBindingName(pattern) {
|
|
13
|
+
if (pattern.kind() !== "object_pattern") return null;
|
|
14
|
+
for (const child of pattern.children()) {
|
|
15
|
+
if (child.kind() === "shorthand_property_identifier_pattern" && child.text() === "relation") return child.text();
|
|
16
|
+
if (child.kind() === "pair_pattern" && stringValue(child.field("key")) === "relation") {
|
|
17
|
+
const value = child.field("value");
|
|
18
|
+
return value?.kind() === "identifier" ? value.text() : null;
|
|
19
|
+
}
|
|
20
|
+
if (child.kind() === "object_assignment_pattern") {
|
|
21
|
+
const binding = child.children().find((node) => node.kind() === "shorthand_property_identifier_pattern");
|
|
22
|
+
if (binding?.text() === "relation") return binding.text();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
function relationAliases(root) {
|
|
28
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
29
|
+
for (const pattern of root.findAll({ rule: { kind: "object_pattern" } })) {
|
|
30
|
+
const name = relationBindingName(pattern);
|
|
31
|
+
if (name) aliases.add(name);
|
|
32
|
+
}
|
|
33
|
+
return aliases;
|
|
34
|
+
}
|
|
35
|
+
function isRelationCall(call, aliases) {
|
|
36
|
+
const callee = call.children()[0];
|
|
37
|
+
if (!callee) return false;
|
|
38
|
+
if (callee.kind() === "identifier") return aliases.has(callee.text());
|
|
39
|
+
if (callee.kind() === "subscript_expression") {
|
|
40
|
+
const property = literalStringValue(callee.field("index"));
|
|
41
|
+
return property === null || property === "relation";
|
|
42
|
+
}
|
|
43
|
+
if (callee.kind() !== "member_expression") return false;
|
|
44
|
+
return callee.children().findLast((child) => child.kind() === "property_identifier" || child.kind() === "identifier")?.text() === "relation";
|
|
45
|
+
}
|
|
46
|
+
function callArgument(call) {
|
|
47
|
+
const args = call.children().find((child) => child.kind() === "arguments");
|
|
48
|
+
if (!args) return null;
|
|
49
|
+
const values = args.children().filter((child) => {
|
|
50
|
+
const kind = child.kind();
|
|
51
|
+
return kind !== "(" && kind !== ")" && kind !== "," && kind !== "comment";
|
|
52
|
+
});
|
|
53
|
+
return values.length === 1 ? values[0] : null;
|
|
54
|
+
}
|
|
55
|
+
function pairKey(pair) {
|
|
56
|
+
const key = pair.children()[0];
|
|
57
|
+
return stringValue(key ?? null);
|
|
58
|
+
}
|
|
59
|
+
function pairValue(pair) {
|
|
60
|
+
const children = pair.children();
|
|
61
|
+
const colonIndex = children.findIndex((child) => child.kind() === ":");
|
|
62
|
+
if (colonIndex === -1) return null;
|
|
63
|
+
return children.slice(colonIndex + 1).find((child) => child.kind() !== "comment") ?? null;
|
|
64
|
+
}
|
|
65
|
+
function objectPair(object, key) {
|
|
66
|
+
return object.children().find((child) => child.kind() === "pair" && pairKey(child) === key) ?? null;
|
|
67
|
+
}
|
|
68
|
+
function literalStringValue(node) {
|
|
69
|
+
if (node?.kind() !== "string") return null;
|
|
70
|
+
return stringValue(node);
|
|
71
|
+
}
|
|
72
|
+
function hasDynamicProperties(object) {
|
|
73
|
+
return object.children().some((child) => {
|
|
74
|
+
const kind = child.kind();
|
|
75
|
+
if (kind === "{" || kind === "}" || kind === "," || kind === "comment") return false;
|
|
76
|
+
if (kind !== "pair") return true;
|
|
77
|
+
const keyKind = child.children()[0]?.kind();
|
|
78
|
+
return keyKind !== "property_identifier" && keyKind !== "string";
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Same as {@link hasDynamicProperties}, but for a `toward` object
|
|
83
|
+
* specifically: a bare `{ type }` shorthand is a safe, rewritable spelling
|
|
84
|
+
* (only the key moves, matching `renameEdit`'s shorthand handling), not a
|
|
85
|
+
* sign of an unsafe/computed key.
|
|
86
|
+
*/
|
|
87
|
+
function hasUnsafeTowardProperties(towardObject) {
|
|
88
|
+
return towardObject.children().some((child) => {
|
|
89
|
+
const kind = child.kind();
|
|
90
|
+
if (kind === "{" || kind === "}" || kind === "," || kind === "comment") return false;
|
|
91
|
+
if (kind === "shorthand_property_identifier") return false;
|
|
92
|
+
if (kind !== "pair") return true;
|
|
93
|
+
const keyKind = child.children()[0]?.kind();
|
|
94
|
+
return keyKind !== "property_identifier" && keyKind !== "string";
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function findLegacyEntry(towardObject) {
|
|
98
|
+
for (const child of towardObject.children()) {
|
|
99
|
+
if (child.kind() === "shorthand_property_identifier" && child.text() === LEGACY_KEY) return {
|
|
100
|
+
node: child,
|
|
101
|
+
shorthand: true
|
|
102
|
+
};
|
|
103
|
+
if (child.kind() !== "pair") continue;
|
|
104
|
+
const key = child.children()[0];
|
|
105
|
+
if (!key) continue;
|
|
106
|
+
if (key.kind() !== "property_identifier" && key.kind() !== "string") continue;
|
|
107
|
+
if (stringValue(key) !== LEGACY_KEY) continue;
|
|
108
|
+
return {
|
|
109
|
+
node: key,
|
|
110
|
+
shorthand: false
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
function renameEdit(entry) {
|
|
116
|
+
if (entry.shorthand) return entry.node.replace(`${NEW_KEY}: ${LEGACY_KEY}`);
|
|
117
|
+
const text = entry.node.text();
|
|
118
|
+
if (entry.node.kind() !== "string") return entry.node.replace(NEW_KEY);
|
|
119
|
+
const quote = text.startsWith("'") ? "'" : text.startsWith("`") ? "`" : "\"";
|
|
120
|
+
return entry.node.replace(`${quote}${NEW_KEY}${quote}`);
|
|
121
|
+
}
|
|
122
|
+
function parseRoot(source, filePath) {
|
|
123
|
+
try {
|
|
124
|
+
return parse(sourceLang(filePath, source), source).root();
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Rename `.relation()`'s `toward.type` option to `toward.table`.
|
|
131
|
+
* @param source - File contents
|
|
132
|
+
* @param filePath - Path to the file being transformed
|
|
133
|
+
* @returns Transformed source, or null when nothing matched
|
|
134
|
+
*/
|
|
135
|
+
function transform(source, filePath = "") {
|
|
136
|
+
if (!source.includes("relation")) return null;
|
|
137
|
+
const root = parseRoot(source, filePath);
|
|
138
|
+
if (!root) return null;
|
|
139
|
+
const aliases = relationAliases(root);
|
|
140
|
+
const edits = [];
|
|
141
|
+
for (const call of root.findAll({ rule: { kind: "call_expression" } })) {
|
|
142
|
+
if (!isRelationCall(call, aliases)) continue;
|
|
143
|
+
const config = callArgument(call);
|
|
144
|
+
if (config?.kind() !== "object") continue;
|
|
145
|
+
if (hasDynamicProperties(config)) continue;
|
|
146
|
+
if (!objectPair(config, "type")) continue;
|
|
147
|
+
const toward = objectPair(config, "toward");
|
|
148
|
+
if (!toward) continue;
|
|
149
|
+
const towardConfig = pairValue(toward);
|
|
150
|
+
if (towardConfig?.kind() !== "object") continue;
|
|
151
|
+
if (hasUnsafeTowardProperties(towardConfig)) continue;
|
|
152
|
+
if (objectPair(towardConfig, NEW_KEY)) continue;
|
|
153
|
+
const entry = findLegacyEntry(towardConfig);
|
|
154
|
+
if (!entry) continue;
|
|
155
|
+
edits.push(renameEdit(entry));
|
|
156
|
+
}
|
|
157
|
+
return edits.length > 0 ? root.commitEdits(edits) : null;
|
|
158
|
+
}
|
|
159
|
+
function lineOf(node) {
|
|
160
|
+
return node.range().start.line + 1;
|
|
161
|
+
}
|
|
162
|
+
function excerptOf(node) {
|
|
163
|
+
return node.text().split("\n", 1)[0].trim();
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Report `.relation()` calls this transform cannot safely rewrite: a
|
|
167
|
+
* non-object call argument, a computed/spread key on the config or `toward`
|
|
168
|
+
* object, or a `toward` reached through something other than a literal
|
|
169
|
+
* object (e.g. a shared variable).
|
|
170
|
+
* @param source - File contents
|
|
171
|
+
* @param filePath - Path to the file being reviewed
|
|
172
|
+
* @param relativePath - Repository-relative path reported to the user
|
|
173
|
+
* @returns Findings for occurrences needing a manual rename
|
|
174
|
+
*/
|
|
175
|
+
function reviewFindings(source, filePath, relativePath) {
|
|
176
|
+
if (!source.includes("relation")) return [];
|
|
177
|
+
const root = parseRoot(source, filePath);
|
|
178
|
+
if (!root) return [];
|
|
179
|
+
const aliases = relationAliases(root);
|
|
180
|
+
const findings = [];
|
|
181
|
+
for (const call of root.findAll({ rule: { kind: "call_expression" } })) {
|
|
182
|
+
if (!isRelationCall(call, aliases)) continue;
|
|
183
|
+
const config = callArgument(call);
|
|
184
|
+
if (config?.kind() !== "object") {
|
|
185
|
+
if (config) findings.push({
|
|
186
|
+
file: relativePath,
|
|
187
|
+
line: lineOf(call),
|
|
188
|
+
message: "This .relation() call's config isn't a literal object; check its toward.type manually.",
|
|
189
|
+
excerpt: excerptOf(call)
|
|
190
|
+
});
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (hasDynamicProperties(config)) {
|
|
194
|
+
findings.push({
|
|
195
|
+
file: relativePath,
|
|
196
|
+
line: lineOf(config),
|
|
197
|
+
message: "A computed/spread key on this .relation() config may hide toward.type.",
|
|
198
|
+
excerpt: excerptOf(config)
|
|
199
|
+
});
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const toward = objectPair(config, "toward");
|
|
203
|
+
if (!toward) continue;
|
|
204
|
+
const towardConfig = pairValue(toward);
|
|
205
|
+
if (towardConfig?.kind() !== "object") {
|
|
206
|
+
findings.push({
|
|
207
|
+
file: relativePath,
|
|
208
|
+
line: lineOf(toward),
|
|
209
|
+
message: "This .relation() call's toward isn't a literal object; rename type to table by hand.",
|
|
210
|
+
excerpt: excerptOf(toward)
|
|
211
|
+
});
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (hasUnsafeTowardProperties(towardConfig)) {
|
|
215
|
+
findings.push({
|
|
216
|
+
file: relativePath,
|
|
217
|
+
line: lineOf(towardConfig),
|
|
218
|
+
message: "A computed/spread key on this toward object may hide type.",
|
|
219
|
+
excerpt: excerptOf(towardConfig)
|
|
220
|
+
});
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (objectPair(towardConfig, NEW_KEY) && findLegacyEntry(towardConfig)) findings.push({
|
|
224
|
+
file: relativePath,
|
|
225
|
+
line: lineOf(towardConfig),
|
|
226
|
+
message: "This toward object has both table and type; remove the deprecated type by hand instead of automatically renaming it (would produce a duplicate key).",
|
|
227
|
+
excerpt: excerptOf(towardConfig)
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return findings;
|
|
231
|
+
}
|
|
232
|
+
//#endregion
|
|
233
|
+
export { transform as default, reviewFindings };
|
package/dist/index.js
CHANGED
|
@@ -672,7 +672,7 @@ const allCodemods = [
|
|
|
672
672
|
after: [
|
|
673
673
|
"ownerId: db.uuid().relation({",
|
|
674
674
|
" type: \"n-1\",",
|
|
675
|
-
" toward: {
|
|
675
|
+
" toward: { table: user, as: \"user\" },",
|
|
676
676
|
"}),"
|
|
677
677
|
].join("\n")
|
|
678
678
|
}],
|
|
@@ -1079,6 +1079,15 @@ const allCodemods = [
|
|
|
1079
1079
|
prereleaseUntil: V2_NEXT_1,
|
|
1080
1080
|
notice: true
|
|
1081
1081
|
},
|
|
1082
|
+
{
|
|
1083
|
+
id: "v2/tailordb-timestamps-required",
|
|
1084
|
+
name: "`db.fields.timestamps()`: `updatedAt` becomes required",
|
|
1085
|
+
description: "The `updatedAt` field from `db.fields.timestamps()` changes from optional to required (non-null): it defaults to the current time and keeps refreshing automatically on every update, though a value you provide explicitly is still respected. Applying this change to a table that already has rows with `updatedAt: null` makes `deploy` fail with `field \"updatedAt\" cannot be updated from non-required to required when records with null values exist`. Backfill those rows first, e.g. `UPDATE <table> SET \"updatedAt\" = \"createdAt\" WHERE \"updatedAt\" IS NULL` for each affected table — see [TailorDB migrations](../services/tailordb-migration.md#performance-and-large-tables) for splitting a large backfill across primary-key ranges if a single `UPDATE` times out.",
|
|
1086
|
+
since: "1.0.0",
|
|
1087
|
+
until: "2.0.0",
|
|
1088
|
+
prereleaseUntil: V2_NEXT_2,
|
|
1089
|
+
notice: true
|
|
1090
|
+
},
|
|
1082
1091
|
{
|
|
1083
1092
|
id: "v2/rename-bin",
|
|
1084
1093
|
name: "tailor-sdk binary → tailor",
|
|
@@ -1606,6 +1615,38 @@ const allCodemods = [
|
|
|
1606
1615
|
"`setup preview`, or `setup coordinate`, which keeps its name, and leave prose",
|
|
1607
1616
|
"that merely mentions the option unchanged unless it documents a command to type."
|
|
1608
1617
|
].join("\n")
|
|
1618
|
+
},
|
|
1619
|
+
{
|
|
1620
|
+
id: "v3/relation-toward-table",
|
|
1621
|
+
name: "relation() toward.type → toward.table",
|
|
1622
|
+
description: "Rename the `.relation()` option `toward.type` to `toward.table`, matching the `db.type()` → `db.table()` rename. The relation's own `type` (its cardinality, e.g. `\"n-1\"`) is unchanged — only the target-table reference nested under `toward` moves.",
|
|
1623
|
+
since: "1.0.0",
|
|
1624
|
+
until: "3.0.0",
|
|
1625
|
+
scriptPath: "v3/relation-toward-table/scripts/transform.js",
|
|
1626
|
+
filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"],
|
|
1627
|
+
examples: [{
|
|
1628
|
+
before: [
|
|
1629
|
+
"customerId: db.uuid().relation({",
|
|
1630
|
+
" type: \"n-1\",",
|
|
1631
|
+
" toward: { type: customer },",
|
|
1632
|
+
"}),"
|
|
1633
|
+
].join("\n"),
|
|
1634
|
+
after: [
|
|
1635
|
+
"customerId: db.uuid().relation({",
|
|
1636
|
+
" type: \"n-1\",",
|
|
1637
|
+
" toward: { table: customer },",
|
|
1638
|
+
"}),"
|
|
1639
|
+
].join("\n")
|
|
1640
|
+
}],
|
|
1641
|
+
prompt: [
|
|
1642
|
+
"In Tailor SDK v3, `.relation()`'s `toward.type` option is renamed to",
|
|
1643
|
+
"`toward.table` (it names a target table, not a TypeScript/GraphQL type).",
|
|
1644
|
+
"Rename any remaining `toward.type` the codemod did not rewrite (e.g. a",
|
|
1645
|
+
"`toward` object reached through a shared variable, spread, or computed",
|
|
1646
|
+
"key) to `toward.table`. Do not touch the relation's own outer `type`",
|
|
1647
|
+
"property, which is the relation's cardinality (e.g. \"n-1\", \"1-1\",",
|
|
1648
|
+
"\"keyOnly\") and keeps its name."
|
|
1649
|
+
].join("\n")
|
|
1609
1650
|
}
|
|
1610
1651
|
];
|
|
1611
1652
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tailor-platform/sdk-codemod",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Codemod runner for Tailor Platform SDK upgrades",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"pathe": "2.0.3",
|
|
22
22
|
"picomatch": "4.0.5",
|
|
23
23
|
"pkg-types": "2.3.1",
|
|
24
|
-
"politty": "0.11.
|
|
24
|
+
"politty": "0.11.9",
|
|
25
25
|
"semver": "7.8.5",
|
|
26
26
|
"zod": "4.4.3"
|
|
27
27
|
},
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
"@types/picomatch": "4.0.3",
|
|
31
31
|
"@types/semver": "7.8.0",
|
|
32
32
|
"eslint-plugin-zod": "4.9.1",
|
|
33
|
-
"oxlint": "1.
|
|
33
|
+
"oxlint": "1.79.0",
|
|
34
34
|
"oxlint-tsgolint": "7.0.2001",
|
|
35
35
|
"tsdown": "0.22.14",
|
|
36
36
|
"typescript": "6.0.3",
|
|
37
|
-
"vitest": "4.1.
|
|
37
|
+
"vitest": "4.1.11",
|
|
38
38
|
"@tailor-platform/shared": "^0.0.0"
|
|
39
39
|
},
|
|
40
40
|
"engines": {
|
|
@@ -48,7 +48,6 @@
|
|
|
48
48
|
"knip": "knip",
|
|
49
49
|
"typecheck": "tsc --noEmit",
|
|
50
50
|
"test": "vitest",
|
|
51
|
-
"prepublish": "pnpm run build",
|
|
52
51
|
"publint": "publint --strict"
|
|
53
52
|
},
|
|
54
53
|
"bin": {
|