Skip to content
Merged
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
7 changes: 5 additions & 2 deletions packages/orm/src/client/executor/name-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,11 @@ export class QueryNameMapper extends OperationNodeTransformer {
// process "from" clauses
const processedFroms = node.from.froms.map((from) => this.processSelectTable(from));

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

// merge the scopes of froms and joins since they're all visible in the query body
const scopes = [...processedFroms.map(({ scope }) => scope), ...processedJoins.map(({ scope }) => scope)];
Expand Down
14 changes: 14 additions & 0 deletions packages/sdk/src/prisma/prisma-schema-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
isArrayExpr,
isDataModel,
isDataSource,
isGeneratorDecl,
isInvocationExpr,
isLiteralExpr,
isNullExpr,
Expand Down Expand Up @@ -106,6 +107,10 @@ export class PrismaSchemaGenerator {
}
}

if (!this.zmodel.declarations.some(isGeneratorDecl)) {
this.generateDefaultGenerator(prisma);
}

return this.PRELUDE + prisma.toString();
}

Expand Down Expand Up @@ -169,6 +174,15 @@ export class PrismaSchemaGenerator {
);
}

private generateDefaultGenerator(prisma: PrismaModel) {
const gen = prisma.addGenerator('client', [{ name: 'provider', text: '"prisma-client-js"' }]);
const dataSource = this.zmodel.declarations.find(isDataSource);
if (dataSource?.fields.some((f) => f.name === 'extensions')) {
// enable "postgresqlExtensions" preview feature
gen.fields.push({ name: 'previewFeatures', text: '["postgresqlExtensions"]' });
}
}

private generateModel(prisma: PrismaModel, decl: DataModel) {
const model = decl.isView ? prisma.addView(decl.name) : prisma.addModel(decl.name);
const allFields = getAllFields(decl, true);
Expand Down
73 changes: 68 additions & 5 deletions packages/testtools/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { SchemaDef } from '@zenstackhq/orm/schema';
import { PolicyPlugin } from '@zenstackhq/plugin-policy';
import { PrismaSchemaGenerator } from '@zenstackhq/sdk';
import SQLite from 'better-sqlite3';
import { glob } from 'glob';
import { PostgresDialect, SqliteDialect, type LogEvent } from 'kysely';
import { execSync } from 'node:child_process';
import { createHash } from 'node:crypto';
Expand Down Expand Up @@ -32,14 +33,55 @@ const TEST_PG_CONFIG = {
};

export type CreateTestClientOptions<Schema extends SchemaDef> = Omit<ClientOptions<Schema>, 'dialect'> & {
/**
* Database provider
*/
provider?: 'sqlite' | 'postgresql';

/**
* The main ZModel file. Only used when `usePrismaPush` is true and `schema` is an object.
*/
schemaFile?: string;

/**
* Database name. If not provided, a name will be generated based on the test name.
*/
dbName?: string;

/**
* Use `prisma db push` instead of ZenStack's `$pushSchema` for database initialization.
*/
usePrismaPush?: boolean;

/**
* Extra source files to create and compile.
*/
extraSourceFiles?: Record<string, string>;

/**
* Working directory for the test client. If not provided, a temporary directory will be created.
*/
workDir?: string;

/**
* Debug mode.
*/
debug?: boolean;

/**
* A sqlite database file to be used for the test. Only supported for sqlite provider.
*/
dbFile?: string;

/**
* PostgreSQL extensions to be added to the datasource. Only supported for postgresql provider.
*/
dataSourceExtensions?: string[];

/**
* Additional files to be copied to the working directory. The glob pattern is relative to the test file.
*/
copyFiles?: { globPattern: string; destination: string }[];
};

export async function createTestClient<Schema extends SchemaDef>(
Expand Down Expand Up @@ -95,16 +137,24 @@ export async function createTestClient<Schema extends SchemaDef>(
`datasource db {
provider = '${provider}'
url = '${dbUrl}'
${options.dataSourceExtensions ? `extensions = [${options.dataSourceExtensions.join(', ')}]` : ''}
}`,
);
}
fs.writeFileSync(path.join(workDir, 'schema.zmodel'), schemaContent);
fs.writeFileSync(path.join(workDir!, 'schema.zmodel'), schemaContent);
}
}

invariant(workDir);

const { plugins, ...rest } = options ?? {};
const _options: ClientOptions<Schema> = {
...rest,
} as ClientOptions<Schema>;

if (options?.debug) {
console.log(`Work directory: ${workDir}`);
_options.log = testLogger;
}

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

const { plugins, ...rest } = options ?? {};
const _options: ClientOptions<Schema> = {
...rest,
} as ClientOptions<Schema>;
// copy additional files if specified
if (options?.copyFiles) {
const state = expect.getState();
const currentTestPath = state.testPath;
if (!currentTestPath) {
throw new Error('Unable to determine current test file path');
}
for (const { globPattern, destination } of options.copyFiles) {
const files = glob.sync(globPattern, { cwd: path.dirname(currentTestPath) });
for (const file of files) {
const src = path.resolve(path.dirname(currentTestPath), file);
const dest = path.resolve(workDir, destination, path.basename(file));
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.copyFileSync(src, dest);
}
}
}

if (!options?.dbFile) {
if (options?.usePrismaPush) {
Expand Down
49 changes: 49 additions & 0 deletions tests/e2e/apps/rally/rally.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { ClientContract } from '@zenstackhq/orm';
import { createTestClient } from '@zenstackhq/testtools';
import path from 'path';
import { beforeEach, describe, expect, it } from 'vitest';
import { schema, type SchemaType } from './zenstack/schema';

describe('Rally app tests', () => {
let db: ClientContract<SchemaType>;

beforeEach(async () => {
db = await createTestClient(schema, {
provider: 'postgresql',
schemaFile: path.join(__dirname, 'zenstack/schema.zmodel'),
copyFiles: [
{
globPattern: 'zenstack/models/*',
destination: 'models',
},
],
debug: true,
dataSourceExtensions: ['citext'],
usePrismaPush: true,
});
});

it('works with queries', async () => {
await expect(
db.spaceMember.findMany({
where: {
userId: '1',
},
orderBy: {
lastSelectedAt: 'desc',
},
include: {
space: {
select: {
id: true,
ownerId: true,
name: true,
tier: true,
image: true,
},
},
},
}),
).toResolveTruthy();
});
});
Loading
Loading