Skip to content

Commit da84ee8

Browse files
authored
Merge pull request #612 from constructive-io/devin/1768387040-orm-babel-migration
refactor(codegen): convert ORM generators from ts-morph to Babel AST
2 parents b9719cd + 03678f6 commit da84ee8

34 files changed

+13336
-10857
lines changed

CLAUDE.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Build & Development Commands
6+
7+
```bash
8+
# Install dependencies
9+
pnpm install
10+
11+
# Build all packages
12+
pnpm build
13+
14+
# Build all packages (dev mode - faster, no optimizations)
15+
pnpm build:dev
16+
17+
# Lint all packages (auto-fix enabled)
18+
pnpm lint
19+
20+
# Clean build artifacts
21+
pnpm clean
22+
23+
# Update dependencies interactively
24+
pnpm deps
25+
```
26+
27+
### Per-Package Commands
28+
29+
Navigate to any package directory (e.g., `cd pgpm/cli`) and run:
30+
31+
```bash
32+
pnpm build # Build the package
33+
pnpm lint # Lint with auto-fix
34+
pnpm test # Run tests
35+
pnpm test:watch # Run tests in watch mode
36+
pnpm dev # Run in development mode (where available)
37+
```
38+
39+
### Running a Single Test File
40+
41+
```bash
42+
cd packages/cli
43+
pnpm test -- path/to/test.test.ts
44+
# or with pattern matching:
45+
pnpm test -- --testNamePattern="test name pattern"
46+
```
47+
48+
## Project Architecture
49+
50+
This is a **pnpm monorepo** using Lerna for versioning/publishing. The workspace is organized into domain-specific directories:
51+
52+
### Core Package Groups
53+
54+
| Directory | Purpose |
55+
|-----------|---------|
56+
| `pgpm/` | PostgreSQL Package Manager - CLI, core engine, types |
57+
| `graphql/` | GraphQL layer - server, codegen, React hooks, testing |
58+
| `graphile/` | PostGraphile plugins - filters, i18n, meta-schema, PostGIS |
59+
| `postgres/` | PostgreSQL utilities - introspection, testing, seeding, AST |
60+
| `packages/` | Shared utilities - CLI, ORM, query builder |
61+
| `uploads/` | File streaming - S3, ETags, content-type detection |
62+
| `jobs/` | Job scheduling and worker infrastructure |
63+
64+
### Key Packages
65+
66+
**pgpm (PostgreSQL Package Manager)**
67+
- `pgpm/cli` - Main CLI tool (`pgpm` command)
68+
- `pgpm/core` - Migration engine, dependency resolution, deployment
69+
70+
**GraphQL Stack**
71+
- `graphql/server` - Express + PostGraphile API server
72+
- `graphql/codegen` - SDK generator (React Query hooks or Prisma-like ORM)
73+
- `graphql/query` - Fluent GraphQL query builder
74+
75+
**Testing Infrastructure**
76+
- `postgres/pgsql-test` - Isolated PostgreSQL test environments with transaction rollback
77+
- `graphile/graphile-test` - GraphQL testing utilities
78+
79+
### Testing Pattern
80+
81+
Tests use `pgsql-test` for database testing with per-test transaction rollback:
82+
83+
```typescript
84+
import { getConnections } from 'pgsql-test';
85+
86+
let db, teardown;
87+
88+
beforeAll(async () => {
89+
({ db, teardown } = await getConnections());
90+
});
91+
92+
beforeEach(() => db.beforeEach());
93+
afterEach(() => db.afterEach());
94+
afterAll(() => teardown());
95+
96+
test('example', async () => {
97+
db.setContext({ role: 'authenticated', 'jwt.claims.user_id': '123' });
98+
const result = await db.query('SELECT current_user_id()');
99+
expect(result.rows[0].current_user_id).toBe('123');
100+
});
101+
```
102+
103+
### Database Configuration
104+
105+
Tests require PostgreSQL. Standard PG environment variables:
106+
- `PGHOST` (default: localhost)
107+
- `PGPORT` (default: 5432)
108+
- `PGUSER` (default: postgres)
109+
- `PGPASSWORD` (default: password)
110+
111+
For S3/MinIO tests: `MINIO_ENDPOINT`, `AWS_ACCESS_KEY`, `AWS_SECRET_KEY`, `AWS_REGION`
112+
113+
### Build System
114+
115+
- Uses `makage` for TypeScript compilation (handles both CJS and ESM output)
116+
- Jest with ts-jest for testing
117+
- ESLint with TypeScript support
118+
- Each package has its own `tsconfig.json` extending root config
119+
120+
### Code Conventions
121+
122+
- TypeScript with `strict: true` (but `strictNullChecks: false`)
123+
- Target: ES2022, Module: CommonJS
124+
- Packages publish to npm from `dist/` directory
125+
- Workspace dependencies use `workspace:^` protocol

graphql/codegen/README.md

Lines changed: 147 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -837,12 +837,12 @@ import { useCreateCarMutation, useCarsQuery } from './generated/hooks';
837837

838838
function CreateCarWithInvalidation() {
839839
const queryClient = useQueryClient();
840-
840+
841841
const createCar = useCreateCarMutation({
842842
onSuccess: () => {
843843
// Invalidate all car queries to refetch
844844
queryClient.invalidateQueries({ queryKey: ['cars'] });
845-
845+
846846
// Or invalidate specific queries
847847
queryClient.invalidateQueries({ queryKey: ['cars', { first: 10 }] });
848848
},
@@ -852,6 +852,151 @@ function CreateCarWithInvalidation() {
852852
}
853853
```
854854

855+
### Centralized Query Keys
856+
857+
The codegen generates a centralized query key factory following the [lukemorales query-key-factory](https://tanstack.com/query/docs/framework/react/community/lukemorales-query-key-factory) pattern. This provides type-safe cache management with autocomplete support.
858+
859+
#### Generated Files
860+
861+
| File | Purpose |
862+
|------|---------|
863+
| `query-keys.ts` | Query key factories for all entities |
864+
| `mutation-keys.ts` | Mutation key factories for tracking in-flight mutations |
865+
| `invalidation.ts` | Type-safe cache invalidation helpers |
866+
867+
#### Using Query Keys
868+
869+
```tsx
870+
import { userKeys, invalidate } from './generated/hooks';
871+
import { useQueryClient } from '@tanstack/react-query';
872+
873+
// Query key structure
874+
userKeys.all // ['user']
875+
userKeys.lists() // ['user', 'list']
876+
userKeys.list({ first: 10 }) // ['user', 'list', { first: 10 }]
877+
userKeys.details() // ['user', 'detail']
878+
userKeys.detail('user-123') // ['user', 'detail', 'user-123']
879+
880+
// Granular cache invalidation
881+
const queryClient = useQueryClient();
882+
883+
// Invalidate ALL user queries
884+
queryClient.invalidateQueries({ queryKey: userKeys.all });
885+
886+
// Invalidate only list queries
887+
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
888+
889+
// Invalidate a specific user
890+
queryClient.invalidateQueries({ queryKey: userKeys.detail(userId) });
891+
```
892+
893+
#### Invalidation Helpers
894+
895+
Type-safe invalidation utilities:
896+
897+
```tsx
898+
import { invalidate, remove } from './generated/hooks';
899+
900+
// Invalidate queries (triggers refetch)
901+
invalidate.user.all(queryClient);
902+
invalidate.user.lists(queryClient);
903+
invalidate.user.detail(queryClient, userId);
904+
905+
// Remove from cache (for delete operations)
906+
remove.user(queryClient, userId);
907+
```
908+
909+
#### Mutation Key Tracking
910+
911+
Track in-flight mutations with `useIsMutating`:
912+
913+
```tsx
914+
import { useIsMutating } from '@tanstack/react-query';
915+
import { userMutationKeys } from './generated/hooks';
916+
917+
function UserList() {
918+
// Check if any user mutations are in progress
919+
const isMutating = useIsMutating({ mutationKey: userMutationKeys.all });
920+
921+
// Check if a specific user is being deleted
922+
const isDeleting = useIsMutating({
923+
mutationKey: userMutationKeys.delete(userId)
924+
});
925+
926+
return (
927+
<div>
928+
{isMutating > 0 && <Spinner />}
929+
<button disabled={isDeleting > 0}>Delete</button>
930+
</div>
931+
);
932+
}
933+
```
934+
935+
#### Optimistic Updates with Query Keys
936+
937+
```tsx
938+
import { useCreateUserMutation, userKeys } from './generated/hooks';
939+
940+
const createUser = useCreateUserMutation({
941+
onMutate: async (newUser) => {
942+
// Cancel outgoing refetches
943+
await queryClient.cancelQueries({ queryKey: userKeys.lists() });
944+
945+
// Snapshot previous value
946+
const previous = queryClient.getQueryData(userKeys.list());
947+
948+
// Optimistically update cache
949+
queryClient.setQueryData(userKeys.list(), (old) => ({
950+
...old,
951+
users: {
952+
...old.users,
953+
nodes: [...old.users.nodes, { id: 'temp', ...newUser.input.user }]
954+
},
955+
}));
956+
957+
return { previous };
958+
},
959+
onError: (err, variables, context) => {
960+
// Rollback on error
961+
queryClient.setQueryData(userKeys.list(), context.previous);
962+
},
963+
onSettled: () => {
964+
// Refetch after mutation
965+
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
966+
},
967+
});
968+
```
969+
970+
#### Configuration
971+
972+
Query key generation is enabled by default. Configure in your config file:
973+
974+
```typescript
975+
// graphql-sdk.config.ts
976+
export default defineConfig({
977+
endpoint: 'https://api.example.com/graphql',
978+
979+
queryKeys: {
980+
// Generate scope-aware keys (default: true)
981+
generateScopedKeys: true,
982+
983+
// Generate mutation keys (default: true)
984+
generateMutationKeys: true,
985+
986+
// Generate invalidation helpers (default: true)
987+
generateCascadeHelpers: true,
988+
989+
// Define entity relationships for cascade invalidation
990+
relationships: {
991+
table: { parent: 'database', foreignKey: 'databaseId' },
992+
field: { parent: 'table', foreignKey: 'tableId' },
993+
},
994+
},
995+
});
996+
```
997+
998+
For detailed documentation on query key factory design and implementation, see [docs/QUERY-KEY-FACTORY.md](./docs/QUERY-KEY-FACTORY.md).
999+
8551000
### Prefetching
8561001

8571002
```tsx

0 commit comments

Comments
 (0)