Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ViewerContext } from '@expo/entity';
import { UserEntity } from './entities/UserEntity';
import { PostEntity } from './entities/PostEntity';

async function loadUser(viewerContext: ViewerContext) {
// Basic loader calls - only transformed when using knex-specific methods
const userLoader = UserEntity.loader(viewerContext);
const postLoader = PostEntity.loader(viewerContext);

// These use knex-specific methods, so they should be transformed
const posts = await postLoader.loadManyByFieldEqualityConjunctionAsync([
{ fieldName: 'status', fieldValue: 'published' }
]);
const firstPost = await postLoader.loadFirstByFieldEqualityConjunctionAsync([
{ fieldName: 'id', fieldValue: '123' }
]);

// Loader with authorization results - only transformed when using knex methods
const userLoaderWithAuth = UserEntity.loaderWithAuthorizationResults(viewerContext);
const rawResults = await userLoaderWithAuth.loadManyByRawWhereClauseAsync('age > ?', [18]);

// Loader that doesn't use knex methods - should NOT be transformed
const standardLoader = PostEntity.loader(viewerContext);
const post = await standardLoader.loadByIDAsync('456');

// Should not transform instance methods or other properties
const user = await userLoader.loadByIDAsync('123');
const userLoadMethod = user.loader; // This should not be transformed

// Should not transform lowercase object methods
const customLoader = {
loader: (ctx: any) => ctx,
};
customLoader.loader(viewerContext);

return user;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ViewerContext } from '@expo/entity';
import { UserEntity } from './entities/UserEntity';
import { PostEntity } from './entities/PostEntity';

async function loadUser(viewerContext: ViewerContext) {
// Basic loader calls - only transformed when using knex-specific methods
const userLoader = UserEntity.loader(viewerContext);
const postLoader = PostEntity.knexLoader(viewerContext);

// These use knex-specific methods, so they should be transformed
const posts = await postLoader.loadManyByFieldEqualityConjunctionAsync([
{ fieldName: 'status', fieldValue: 'published' }
]);
const firstPost = await postLoader.loadFirstByFieldEqualityConjunctionAsync([
{ fieldName: 'id', fieldValue: '123' }
]);

// Loader with authorization results - only transformed when using knex methods
const userLoaderWithAuth = UserEntity.knexLoaderWithAuthorizationResults(viewerContext);
const rawResults = await userLoaderWithAuth.loadManyByRawWhereClauseAsync('age > ?', [18]);

// Loader that doesn't use knex methods - should NOT be transformed
const standardLoader = PostEntity.loader(viewerContext);
const post = await standardLoader.loadByIDAsync('456');

// Should not transform instance methods or other properties
const user = await userLoader.loadByIDAsync('123');
const userLoadMethod = user.loader; // This should not be transformed

// Should not transform lowercase object methods
const customLoader = {
loader: (ctx: any) => ctx,
};
customLoader.loader(viewerContext);

return user;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ViewerContext } from '@expo/entity';
import { CommentEntity } from './entities/CommentEntity';

// Chained calls
const loadComments = async (viewerContext: ViewerContext) => {
// Direct chaining with knex-specific method
const comments = await CommentEntity.loader(viewerContext)
.loadManyByFieldEqualityConjunctionAsync([
{ fieldName: 'postId', fieldValue: '123' }
]);

// Direct chaining with regular method - should NOT be transformed
const singleComment = await CommentEntity
.loader(viewerContext)
.loadByIDAsync('456');

// With authorization results and knex method
const commentsWithAuth = await CommentEntity
.loaderWithAuthorizationResults(viewerContext)
.loadManyByRawWhereClauseAsync('postId = ?', ['456']);

// Edge cases - these should NOT be transformed
const anotherEntity = {
loader: (ctx: any) => ctx, // This is not an entity class
};
anotherEntity.loader(viewerContext); // Should NOT be transformed (lowercase object)

// Complex chaining with regular method - should NOT be transformed
return CommentEntity
.loader(viewerContext)
.withAuthenticationResults()
.loadByIDAsync('789');
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ViewerContext } from '@expo/entity';
import { CommentEntity } from './entities/CommentEntity';

// Chained calls
const loadComments = async (viewerContext: ViewerContext) => {
// Direct chaining with knex-specific method
const comments = await CommentEntity.knexLoader(viewerContext)
.loadManyByFieldEqualityConjunctionAsync([
{ fieldName: 'postId', fieldValue: '123' }
]);

// Direct chaining with regular method - should NOT be transformed
const singleComment = await CommentEntity
.loader(viewerContext)
.loadByIDAsync('456');

// With authorization results and knex method
const commentsWithAuth = await CommentEntity
.knexLoaderWithAuthorizationResults(viewerContext)
.loadManyByRawWhereClauseAsync('postId = ?', ['456']);

// Edge cases - these should NOT be transformed
const anotherEntity = {
loader: (ctx: any) => ctx, // This is not an entity class
};
anotherEntity.loader(viewerContext); // Should NOT be transformed (lowercase object)

// Complex chaining with regular method - should NOT be transformed
return CommentEntity
.loader(viewerContext)
.withAuthenticationResults()
.loadByIDAsync('789');
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { jest } from '@jest/globals';
import { readdirSync } from 'fs';
import { join } from 'path';

jest.autoMockOff();
const defineTest = require('jscodeshift/dist/testUtils').defineTest;

const fixtureDir = 'v0.55.0-v0.56.0';
const fixtureDirPath = join(__dirname, '..', '__testfixtures__', fixtureDir);
const fixtures = readdirSync(fixtureDirPath)
.filter((file) => file.endsWith('.input.ts'))
.map((file) => file.replace('.input.ts', ''));

for (const fixture of fixtures) {
const prefix = `${fixtureDir}/${fixture}`;
defineTest(__dirname, 'v0.55.0-v0.56.0', null, prefix, { parser: 'ts' });
}
169 changes: 169 additions & 0 deletions packages/entity-codemod/src/transforms/v0.55.0-v0.56.0.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { API, Collection, FileInfo, Options } from 'jscodeshift';

const KNEX_SPECIFIC_METHODS = [
'loadFirstByFieldEqualityConjunctionAsync',
'loadManyByFieldEqualityConjunctionAsync',
'loadManyByRawWhereClauseAsync',
];

function isKnexSpecificMethodUsed(j: API['jscodeshift'], node: any): boolean {
// Check if this loader call is followed by a knex-specific method
// We need to traverse the AST to find usages of the loader result
const parent = node.parent;

// Check if the loader call is directly chained with a knex method
if (parent && parent.value.type === 'MemberExpression' && parent.value.object === node.value) {
const grandParent = parent.parent;
if (
grandParent &&
grandParent.value.type === 'CallExpression' &&
grandParent.value.callee === parent.value
) {
if (
parent.value.property.type === 'Identifier' &&
KNEX_SPECIFIC_METHODS.includes(parent.value.property.name)
) {
return true;
}
}
}

// Check if the loader is assigned to a variable and then used with knex methods
if (parent && parent.value.type === 'VariableDeclarator' && parent.value.init === node.value) {
const variableName = parent.value.id.name;
const scope = parent.scope;

// Find all references to this variable in the same scope
const references = j(scope.path)
.find(j.Identifier, { name: variableName })
.filter((path) => {
// Check if this identifier is used as object in member expression
const parentNode = path.parent.value;
if (parentNode.type === 'MemberExpression' && parentNode.object === path.value) {
const prop = parentNode.property;
if (prop.type === 'Identifier' && KNEX_SPECIFIC_METHODS.includes(prop.name)) {
return true;
}
}
return false;
});

if (references.size() > 0) {
return true;
}
}

// Check await expressions
if (parent && parent.value.type === 'AwaitExpression' && parent.value.argument === node.value) {
const awaitParent = parent.parent;
if (awaitParent && awaitParent.value.type === 'VariableDeclarator') {
const variableName = awaitParent.value.id.name;
const scope = awaitParent.scope;

// Find all references to this variable in the same scope
const references = j(scope.path)
.find(j.Identifier, { name: variableName })
.filter((path) => {
const parentNode = path.parent.value;
if (parentNode.type === 'MemberExpression' && parentNode.object === path.value) {
const prop = parentNode.property;
if (prop.type === 'Identifier' && KNEX_SPECIFIC_METHODS.includes(prop.name)) {
return true;
}
}
return false;
});

if (references.size() > 0) {
return true;
}
}
}

return false;
}

function transformLoaderToKnexLoader(j: API['jscodeshift'], root: Collection<any>): void {
// Find all entity expressions of the form `Entity.loader(viewerContext)`
root
.find(j.CallExpression, {
callee: {
type: 'MemberExpression',
property: {
name: 'loader',
},
},
})
.forEach((path) => {
const loaderCallExpression = path.node; // Entity.loader(viewerContext)
const loaderCallee = loaderCallExpression.callee; // Entity.loader

if (loaderCallee.type !== 'MemberExpression') {
return;
}

// Make sure this is a static method call on an entity (not on an instance)
// Typically entity names start with uppercase letter
if (loaderCallee.object.type === 'Identifier') {
const firstChar = loaderCallee.object.name[0];
if (firstChar && firstChar === firstChar.toUpperCase()) {
// Check if this loader uses knex-specific methods
if (isKnexSpecificMethodUsed(j, path)) {
// Rename loader to knexLoader
if (loaderCallee.property.type === 'Identifier') {
loaderCallee.property.name = 'knexLoader';
}
}
}
}
});
}

function transformLoaderWithAuthorizationResultsToKnexLoaderWithAuthorizationResults(
j: API['jscodeshift'],
root: Collection<any>,
): void {
// Find all entity expressions of the form `Entity.loaderWithAuthorizationResults(viewerContext)`
root
.find(j.CallExpression, {
callee: {
type: 'MemberExpression',
property: {
name: 'loaderWithAuthorizationResults',
},
},
})
.forEach((path) => {
const loaderCallExpression = path.node; // Entity.loaderWithAuthorizationResults(viewerContext)
const loaderCallee = loaderCallExpression.callee; // Entity.loaderWithAuthorizationResults

if (loaderCallee.type !== 'MemberExpression') {
return;
}

// Make sure this is a static method call on an entity (not on an instance)
// Typically entity names start with uppercase letter
if (loaderCallee.object.type === 'Identifier') {
const firstChar = loaderCallee.object.name[0];
if (firstChar && firstChar === firstChar.toUpperCase()) {
// Check if this loader uses knex-specific methods
if (isKnexSpecificMethodUsed(j, path)) {
// Rename loaderWithAuthorizationResults to knexLoaderWithAuthorizationResults
if (loaderCallee.property.type === 'Identifier') {
loaderCallee.property.name = 'knexLoaderWithAuthorizationResults';
}
}
}
}
});
}

export default function transformer(file: FileInfo, api: API, _options: Options): string {
const j = api.jscodeshift;
const root = j.withParser('ts')(file.source);

transformLoaderToKnexLoader(j, root);
transformLoaderWithAuthorizationResultsToKnexLoaderWithAuthorizationResults(j, root);

return root.toSource();
}