Skip to content

Commit aba0ce5

Browse files
authored
feat: add codemod for upcoming v0.55.0-v0.56.0 (#413)
# Why This adds a codemod for #407-#410 changes. # How Replace `.loader` calls with `.knexLoader` where applicable. # Test Plan Run the test.
1 parent eeccf59 commit aba0ce5

6 files changed

Lines changed: 326 additions & 0 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { ViewerContext } from '@expo/entity';
2+
import { UserEntity } from './entities/UserEntity';
3+
import { PostEntity } from './entities/PostEntity';
4+
5+
async function loadUser(viewerContext: ViewerContext) {
6+
// Basic loader calls - only transformed when using knex-specific methods
7+
const userLoader = UserEntity.loader(viewerContext);
8+
const postLoader = PostEntity.loader(viewerContext);
9+
10+
// These use knex-specific methods, so they should be transformed
11+
const posts = await postLoader.loadManyByFieldEqualityConjunctionAsync([
12+
{ fieldName: 'status', fieldValue: 'published' }
13+
]);
14+
const firstPost = await postLoader.loadFirstByFieldEqualityConjunctionAsync([
15+
{ fieldName: 'id', fieldValue: '123' }
16+
]);
17+
18+
// Loader with authorization results - only transformed when using knex methods
19+
const userLoaderWithAuth = UserEntity.loaderWithAuthorizationResults(viewerContext);
20+
const rawResults = await userLoaderWithAuth.loadManyByRawWhereClauseAsync('age > ?', [18]);
21+
22+
// Loader that doesn't use knex methods - should NOT be transformed
23+
const standardLoader = PostEntity.loader(viewerContext);
24+
const post = await standardLoader.loadByIDAsync('456');
25+
26+
// Should not transform instance methods or other properties
27+
const user = await userLoader.loadByIDAsync('123');
28+
const userLoadMethod = user.loader; // This should not be transformed
29+
30+
// Should not transform lowercase object methods
31+
const customLoader = {
32+
loader: (ctx: any) => ctx,
33+
};
34+
customLoader.loader(viewerContext);
35+
36+
return user;
37+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { ViewerContext } from '@expo/entity';
2+
import { UserEntity } from './entities/UserEntity';
3+
import { PostEntity } from './entities/PostEntity';
4+
5+
async function loadUser(viewerContext: ViewerContext) {
6+
// Basic loader calls - only transformed when using knex-specific methods
7+
const userLoader = UserEntity.loader(viewerContext);
8+
const postLoader = PostEntity.knexLoader(viewerContext);
9+
10+
// These use knex-specific methods, so they should be transformed
11+
const posts = await postLoader.loadManyByFieldEqualityConjunctionAsync([
12+
{ fieldName: 'status', fieldValue: 'published' }
13+
]);
14+
const firstPost = await postLoader.loadFirstByFieldEqualityConjunctionAsync([
15+
{ fieldName: 'id', fieldValue: '123' }
16+
]);
17+
18+
// Loader with authorization results - only transformed when using knex methods
19+
const userLoaderWithAuth = UserEntity.knexLoaderWithAuthorizationResults(viewerContext);
20+
const rawResults = await userLoaderWithAuth.loadManyByRawWhereClauseAsync('age > ?', [18]);
21+
22+
// Loader that doesn't use knex methods - should NOT be transformed
23+
const standardLoader = PostEntity.loader(viewerContext);
24+
const post = await standardLoader.loadByIDAsync('456');
25+
26+
// Should not transform instance methods or other properties
27+
const user = await userLoader.loadByIDAsync('123');
28+
const userLoadMethod = user.loader; // This should not be transformed
29+
30+
// Should not transform lowercase object methods
31+
const customLoader = {
32+
loader: (ctx: any) => ctx,
33+
};
34+
customLoader.loader(viewerContext);
35+
36+
return user;
37+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { ViewerContext } from '@expo/entity';
2+
import { CommentEntity } from './entities/CommentEntity';
3+
4+
// Chained calls
5+
const loadComments = async (viewerContext: ViewerContext) => {
6+
// Direct chaining with knex-specific method
7+
const comments = await CommentEntity.loader(viewerContext)
8+
.loadManyByFieldEqualityConjunctionAsync([
9+
{ fieldName: 'postId', fieldValue: '123' }
10+
]);
11+
12+
// Direct chaining with regular method - should NOT be transformed
13+
const singleComment = await CommentEntity
14+
.loader(viewerContext)
15+
.loadByIDAsync('456');
16+
17+
// With authorization results and knex method
18+
const commentsWithAuth = await CommentEntity
19+
.loaderWithAuthorizationResults(viewerContext)
20+
.loadManyByRawWhereClauseAsync('postId = ?', ['456']);
21+
22+
// Edge cases - these should NOT be transformed
23+
const anotherEntity = {
24+
loader: (ctx: any) => ctx, // This is not an entity class
25+
};
26+
anotherEntity.loader(viewerContext); // Should NOT be transformed (lowercase object)
27+
28+
// Complex chaining with regular method - should NOT be transformed
29+
return CommentEntity
30+
.loader(viewerContext)
31+
.withAuthenticationResults()
32+
.loadByIDAsync('789');
33+
};
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { ViewerContext } from '@expo/entity';
2+
import { CommentEntity } from './entities/CommentEntity';
3+
4+
// Chained calls
5+
const loadComments = async (viewerContext: ViewerContext) => {
6+
// Direct chaining with knex-specific method
7+
const comments = await CommentEntity.knexLoader(viewerContext)
8+
.loadManyByFieldEqualityConjunctionAsync([
9+
{ fieldName: 'postId', fieldValue: '123' }
10+
]);
11+
12+
// Direct chaining with regular method - should NOT be transformed
13+
const singleComment = await CommentEntity
14+
.loader(viewerContext)
15+
.loadByIDAsync('456');
16+
17+
// With authorization results and knex method
18+
const commentsWithAuth = await CommentEntity
19+
.knexLoaderWithAuthorizationResults(viewerContext)
20+
.loadManyByRawWhereClauseAsync('postId = ?', ['456']);
21+
22+
// Edge cases - these should NOT be transformed
23+
const anotherEntity = {
24+
loader: (ctx: any) => ctx, // This is not an entity class
25+
};
26+
anotherEntity.loader(viewerContext); // Should NOT be transformed (lowercase object)
27+
28+
// Complex chaining with regular method - should NOT be transformed
29+
return CommentEntity
30+
.loader(viewerContext)
31+
.withAuthenticationResults()
32+
.loadByIDAsync('789');
33+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { jest } from '@jest/globals';
2+
import { readdirSync } from 'fs';
3+
import { join } from 'path';
4+
5+
jest.autoMockOff();
6+
const defineTest = require('jscodeshift/dist/testUtils').defineTest;
7+
8+
const fixtureDir = 'v0.55.0-v0.56.0';
9+
const fixtureDirPath = join(__dirname, '..', '__testfixtures__', fixtureDir);
10+
const fixtures = readdirSync(fixtureDirPath)
11+
.filter((file) => file.endsWith('.input.ts'))
12+
.map((file) => file.replace('.input.ts', ''));
13+
14+
for (const fixture of fixtures) {
15+
const prefix = `${fixtureDir}/${fixture}`;
16+
defineTest(__dirname, 'v0.55.0-v0.56.0', null, prefix, { parser: 'ts' });
17+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { API, Collection, FileInfo, Options } from 'jscodeshift';
2+
3+
const KNEX_SPECIFIC_METHODS = [
4+
'loadFirstByFieldEqualityConjunctionAsync',
5+
'loadManyByFieldEqualityConjunctionAsync',
6+
'loadManyByRawWhereClauseAsync',
7+
];
8+
9+
function isKnexSpecificMethodUsed(j: API['jscodeshift'], node: any): boolean {
10+
// Check if this loader call is followed by a knex-specific method
11+
// We need to traverse the AST to find usages of the loader result
12+
const parent = node.parent;
13+
14+
// Check if the loader call is directly chained with a knex method
15+
if (parent && parent.value.type === 'MemberExpression' && parent.value.object === node.value) {
16+
const grandParent = parent.parent;
17+
if (
18+
grandParent &&
19+
grandParent.value.type === 'CallExpression' &&
20+
grandParent.value.callee === parent.value
21+
) {
22+
if (
23+
parent.value.property.type === 'Identifier' &&
24+
KNEX_SPECIFIC_METHODS.includes(parent.value.property.name)
25+
) {
26+
return true;
27+
}
28+
}
29+
}
30+
31+
// Check if the loader is assigned to a variable and then used with knex methods
32+
if (parent && parent.value.type === 'VariableDeclarator' && parent.value.init === node.value) {
33+
const variableName = parent.value.id.name;
34+
const scope = parent.scope;
35+
36+
// Find all references to this variable in the same scope
37+
const references = j(scope.path)
38+
.find(j.Identifier, { name: variableName })
39+
.filter((path) => {
40+
// Check if this identifier is used as object in member expression
41+
const parentNode = path.parent.value;
42+
if (parentNode.type === 'MemberExpression' && parentNode.object === path.value) {
43+
const prop = parentNode.property;
44+
if (prop.type === 'Identifier' && KNEX_SPECIFIC_METHODS.includes(prop.name)) {
45+
return true;
46+
}
47+
}
48+
return false;
49+
});
50+
51+
if (references.size() > 0) {
52+
return true;
53+
}
54+
}
55+
56+
// Check await expressions
57+
if (parent && parent.value.type === 'AwaitExpression' && parent.value.argument === node.value) {
58+
const awaitParent = parent.parent;
59+
if (awaitParent && awaitParent.value.type === 'VariableDeclarator') {
60+
const variableName = awaitParent.value.id.name;
61+
const scope = awaitParent.scope;
62+
63+
// Find all references to this variable in the same scope
64+
const references = j(scope.path)
65+
.find(j.Identifier, { name: variableName })
66+
.filter((path) => {
67+
const parentNode = path.parent.value;
68+
if (parentNode.type === 'MemberExpression' && parentNode.object === path.value) {
69+
const prop = parentNode.property;
70+
if (prop.type === 'Identifier' && KNEX_SPECIFIC_METHODS.includes(prop.name)) {
71+
return true;
72+
}
73+
}
74+
return false;
75+
});
76+
77+
if (references.size() > 0) {
78+
return true;
79+
}
80+
}
81+
}
82+
83+
return false;
84+
}
85+
86+
function transformLoaderToKnexLoader(j: API['jscodeshift'], root: Collection<any>): void {
87+
// Find all entity expressions of the form `Entity.loader(viewerContext)`
88+
root
89+
.find(j.CallExpression, {
90+
callee: {
91+
type: 'MemberExpression',
92+
property: {
93+
name: 'loader',
94+
},
95+
},
96+
})
97+
.forEach((path) => {
98+
const loaderCallExpression = path.node; // Entity.loader(viewerContext)
99+
const loaderCallee = loaderCallExpression.callee; // Entity.loader
100+
101+
if (loaderCallee.type !== 'MemberExpression') {
102+
return;
103+
}
104+
105+
// Make sure this is a static method call on an entity (not on an instance)
106+
// Typically entity names start with uppercase letter
107+
if (loaderCallee.object.type === 'Identifier') {
108+
const firstChar = loaderCallee.object.name[0];
109+
if (firstChar && firstChar === firstChar.toUpperCase()) {
110+
// Check if this loader uses knex-specific methods
111+
if (isKnexSpecificMethodUsed(j, path)) {
112+
// Rename loader to knexLoader
113+
if (loaderCallee.property.type === 'Identifier') {
114+
loaderCallee.property.name = 'knexLoader';
115+
}
116+
}
117+
}
118+
}
119+
});
120+
}
121+
122+
function transformLoaderWithAuthorizationResultsToKnexLoaderWithAuthorizationResults(
123+
j: API['jscodeshift'],
124+
root: Collection<any>,
125+
): void {
126+
// Find all entity expressions of the form `Entity.loaderWithAuthorizationResults(viewerContext)`
127+
root
128+
.find(j.CallExpression, {
129+
callee: {
130+
type: 'MemberExpression',
131+
property: {
132+
name: 'loaderWithAuthorizationResults',
133+
},
134+
},
135+
})
136+
.forEach((path) => {
137+
const loaderCallExpression = path.node; // Entity.loaderWithAuthorizationResults(viewerContext)
138+
const loaderCallee = loaderCallExpression.callee; // Entity.loaderWithAuthorizationResults
139+
140+
if (loaderCallee.type !== 'MemberExpression') {
141+
return;
142+
}
143+
144+
// Make sure this is a static method call on an entity (not on an instance)
145+
// Typically entity names start with uppercase letter
146+
if (loaderCallee.object.type === 'Identifier') {
147+
const firstChar = loaderCallee.object.name[0];
148+
if (firstChar && firstChar === firstChar.toUpperCase()) {
149+
// Check if this loader uses knex-specific methods
150+
if (isKnexSpecificMethodUsed(j, path)) {
151+
// Rename loaderWithAuthorizationResults to knexLoaderWithAuthorizationResults
152+
if (loaderCallee.property.type === 'Identifier') {
153+
loaderCallee.property.name = 'knexLoaderWithAuthorizationResults';
154+
}
155+
}
156+
}
157+
}
158+
});
159+
}
160+
161+
export default function transformer(file: FileInfo, api: API, _options: Options): string {
162+
const j = api.jscodeshift;
163+
const root = j.withParser('ts')(file.source);
164+
165+
transformLoaderToKnexLoader(j, root);
166+
transformLoaderWithAuthorizationResultsToKnexLoaderWithAuthorizationResults(j, root);
167+
168+
return root.toSource();
169+
}

0 commit comments

Comments
 (0)