Skip to content

Commit 6166b28

Browse files
committed
fix(orm): foreign key name mapping is not properly process in join
1 parent aa34372 commit 6166b28

File tree

21 files changed

+3985
-14
lines changed

21 files changed

+3985
-14
lines changed

packages/orm/src/client/executor/name-mapper.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,11 @@ export class QueryNameMapper extends OperationNodeTransformer {
7777
// process "from" clauses
7878
const processedFroms = node.from.froms.map((from) => this.processSelectTable(from));
7979

80-
// process "join" clauses
81-
const processedJoins = (node.joins ?? []).map((join) => this.processSelectTable(join.table));
80+
// process "join" clauses, note that "from" needs to be added as scopes since join conditions
81+
// can refer to "from" tables
82+
const processedJoins = this.withScopes([...processedFroms.map(({ scope }) => scope)], () =>
83+
(node.joins ?? []).map((join) => this.processSelectTable(join.table)),
84+
);
8285

8386
// merge the scopes of froms and joins since they're all visible in the query body
8487
const scopes = [...processedFroms.map(({ scope }) => scope), ...processedJoins.map(({ scope }) => scope)];

packages/sdk/src/prisma/prisma-schema-generator.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
isArrayExpr,
2121
isDataModel,
2222
isDataSource,
23+
isGeneratorDecl,
2324
isInvocationExpr,
2425
isLiteralExpr,
2526
isNullExpr,
@@ -106,6 +107,10 @@ export class PrismaSchemaGenerator {
106107
}
107108
}
108109

110+
if (!this.zmodel.declarations.some(isGeneratorDecl)) {
111+
this.generateDefaultGenerator(prisma);
112+
}
113+
109114
return this.PRELUDE + prisma.toString();
110115
}
111116

@@ -169,6 +174,15 @@ export class PrismaSchemaGenerator {
169174
);
170175
}
171176

177+
private generateDefaultGenerator(prisma: PrismaModel) {
178+
const gen = prisma.addGenerator('client', [{ name: 'provider', text: '"prisma-client-js"' }]);
179+
const dataSource = this.zmodel.declarations.find(isDataSource);
180+
if (dataSource?.fields.some((f) => f.name === 'extensions')) {
181+
// enable "postgresqlExtensions" preview feature
182+
gen.fields.push({ name: 'previewFeatures', text: '["postgresqlExtensions"]' });
183+
}
184+
}
185+
172186
private generateModel(prisma: PrismaModel, decl: DataModel) {
173187
const model = decl.isView ? prisma.addView(decl.name) : prisma.addModel(decl.name);
174188
const allFields = getAllFields(decl, true);

packages/testtools/src/client.ts

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { SchemaDef } from '@zenstackhq/orm/schema';
55
import { PolicyPlugin } from '@zenstackhq/plugin-policy';
66
import { PrismaSchemaGenerator } from '@zenstackhq/sdk';
77
import SQLite from 'better-sqlite3';
8+
import { glob } from 'glob';
89
import { PostgresDialect, SqliteDialect, type LogEvent } from 'kysely';
910
import { execSync } from 'node:child_process';
1011
import { createHash } from 'node:crypto';
@@ -32,14 +33,55 @@ const TEST_PG_CONFIG = {
3233
};
3334

3435
export type CreateTestClientOptions<Schema extends SchemaDef> = Omit<ClientOptions<Schema>, 'dialect'> & {
36+
/**
37+
* Database provider
38+
*/
3539
provider?: 'sqlite' | 'postgresql';
40+
41+
/**
42+
* The main ZModel file. Only used when `usePrismaPush` is true and `schema` is an object.
43+
*/
3644
schemaFile?: string;
45+
46+
/**
47+
* Database name. If not provided, a name will be generated based on the test name.
48+
*/
3749
dbName?: string;
50+
51+
/**
52+
* Use `prisma db push` instead of ZenStack's `$pushSchema` for database initialization.
53+
*/
3854
usePrismaPush?: boolean;
55+
56+
/**
57+
* Extra source files to create and compile.
58+
*/
3959
extraSourceFiles?: Record<string, string>;
60+
61+
/**
62+
* Working directory for the test client. If not provided, a temporary directory will be created.
63+
*/
4064
workDir?: string;
65+
66+
/**
67+
* Debug mode.
68+
*/
4169
debug?: boolean;
70+
71+
/**
72+
* A sqlite database file to be used for the test. Only supported for sqlite provider.
73+
*/
4274
dbFile?: string;
75+
76+
/**
77+
* PostgreSQL extensions to be added to the datasource. Only supported for postgresql provider.
78+
*/
79+
dataSourceExtensions?: string[];
80+
81+
/**
82+
* Additional files to be copied to the working directory. The glob pattern is relative to the test file.
83+
*/
84+
copyFiles?: { globPattern: string; destination: string }[];
4385
};
4486

4587
export async function createTestClient<Schema extends SchemaDef>(
@@ -95,16 +137,24 @@ export async function createTestClient<Schema extends SchemaDef>(
95137
`datasource db {
96138
provider = '${provider}'
97139
url = '${dbUrl}'
140+
${options.dataSourceExtensions ? `extensions = [${options.dataSourceExtensions.join(', ')}]` : ''}
98141
}`,
99142
);
100143
}
101-
fs.writeFileSync(path.join(workDir, 'schema.zmodel'), schemaContent);
144+
fs.writeFileSync(path.join(workDir!, 'schema.zmodel'), schemaContent);
102145
}
103146
}
104147

105148
invariant(workDir);
149+
150+
const { plugins, ...rest } = options ?? {};
151+
const _options: ClientOptions<Schema> = {
152+
...rest,
153+
} as ClientOptions<Schema>;
154+
106155
if (options?.debug) {
107156
console.log(`Work directory: ${workDir}`);
157+
_options.log = testLogger;
108158
}
109159

110160
// copy db file to workDir if specified
@@ -115,10 +165,23 @@ export async function createTestClient<Schema extends SchemaDef>(
115165
fs.copyFileSync(options.dbFile, path.join(workDir, dbName));
116166
}
117167

118-
const { plugins, ...rest } = options ?? {};
119-
const _options: ClientOptions<Schema> = {
120-
...rest,
121-
} as ClientOptions<Schema>;
168+
// copy additional files if specified
169+
if (options?.copyFiles) {
170+
const state = expect.getState();
171+
const currentTestPath = state.testPath;
172+
if (!currentTestPath) {
173+
throw new Error('Unable to determine current test file path');
174+
}
175+
for (const { globPattern, destination } of options.copyFiles) {
176+
const files = glob.sync(globPattern, { cwd: path.dirname(currentTestPath) });
177+
for (const file of files) {
178+
const src = path.resolve(path.dirname(currentTestPath), file);
179+
const dest = path.resolve(workDir, destination, path.basename(file));
180+
fs.mkdirSync(path.dirname(dest), { recursive: true });
181+
fs.copyFileSync(src, dest);
182+
}
183+
}
184+
}
122185

123186
if (!options?.dbFile) {
124187
if (options?.usePrismaPush) {

tests/e2e/apps/rally/rally.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type { ClientContract } from '@zenstackhq/orm';
2+
import { createTestClient } from '@zenstackhq/testtools';
3+
import path from 'path';
4+
import { beforeEach, describe, expect, it } from 'vitest';
5+
import { schema, type SchemaType } from './zenstack/schema';
6+
7+
describe('Rally app tests', () => {
8+
let db: ClientContract<SchemaType>;
9+
10+
beforeEach(async () => {
11+
db = await createTestClient(schema, {
12+
provider: 'postgresql',
13+
schemaFile: path.join(__dirname, 'zenstack/schema.zmodel'),
14+
copyFiles: [
15+
{
16+
globPattern: 'zenstack/models/*',
17+
destination: 'models',
18+
},
19+
],
20+
debug: true,
21+
dataSourceExtensions: ['citext'],
22+
usePrismaPush: true,
23+
});
24+
});
25+
26+
it('works with queries', async () => {
27+
await expect(
28+
db.spaceMember.findMany({
29+
where: {
30+
userId: '1',
31+
},
32+
orderBy: {
33+
lastSelectedAt: 'desc',
34+
},
35+
include: {
36+
space: {
37+
select: {
38+
id: true,
39+
ownerId: true,
40+
name: true,
41+
tier: true,
42+
image: true,
43+
},
44+
},
45+
},
46+
}),
47+
).toResolveTruthy();
48+
});
49+
});

0 commit comments

Comments
 (0)