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
23 changes: 18 additions & 5 deletions packages/cli/src/actions/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ type Options = {
output?: string;
silent: boolean;
watch: boolean;
lite: boolean;
liteOnly: boolean;
lite?: boolean;
liteOnly?: boolean;
generateModels?: boolean;
generateInput?: boolean;
};

/**
Expand Down Expand Up @@ -181,12 +183,18 @@ async function runPlugins(schemaFile: string, model: Model, outputPath: string,

// merge CLI options
if (provider === '@core/typescript') {
if (pluginOptions['lite'] === undefined) {
if (options.lite !== undefined) {
pluginOptions['lite'] = options.lite;
}
if (pluginOptions['liteOnly'] === undefined) {
if (options.liteOnly !== undefined) {
pluginOptions['liteOnly'] = options.liteOnly;
}
if (options.generateModels !== undefined) {
pluginOptions['generateModels'] = options.generateModels;
}
if (options.generateInput !== undefined) {
pluginOptions['generateInput'] = options.generateInput;
}
}

processedPlugins.push({ cliPlugin, pluginOptions });
Expand All @@ -196,7 +204,12 @@ async function runPlugins(schemaFile: string, model: Model, outputPath: string,
const defaultPlugins = [
{
plugin: corePlugins['typescript'],
options: { lite: options.lite, liteOnly: options.liteOnly },
options: {
lite: options.lite,
liteOnly: options.liteOnly,
generateModels: options.generateModels,
generateInput: options.generateInput,
},
},
];
defaultPlugins.forEach(({ plugin, options }) => {
Expand Down
24 changes: 22 additions & 2 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ const proxyAction = async (options: Parameters<typeof actions.proxy>[0]): Promis
await telemetry.trackCommand('proxy', () => actions.proxy(options));
};

function triStateBooleanOption(flag: string, description: string) {
return new Option(flag, description).choices(['true', 'false']).argParser((value) => {
if (value === undefined || value === 'true') return true;
if (value === 'false') return false;
throw new CliError(`Invalid value for ${flag}: ${value}`);
});
}

function createProgram() {
const program = new Command('zen')
.alias('zenstack')
Expand Down Expand Up @@ -74,8 +82,20 @@ function createProgram() {
.addOption(noVersionCheckOption)
.addOption(new Option('-o, --output <path>', 'default output directory for code generation'))
.addOption(new Option('-w, --watch', 'enable watch mode').default(false))
.addOption(new Option('--lite', 'also generate a lite version of schema without attributes').default(false))
.addOption(new Option('--lite-only', 'only generate lite version of schema without attributes').default(false))
.addOption(
triStateBooleanOption(
'--lite [boolean]',
'also generate a lite version of schema without attributes, defaults to false',
),
)
.addOption(
triStateBooleanOption(
'--lite-only [boolean]',
'only generate lite version of schema without attributes, defaults to false',
),
)
.addOption(triStateBooleanOption('--generate-models [boolean]', 'generate models.ts file, defaults to true'))
.addOption(triStateBooleanOption('--generate-input [boolean]', 'generate input.ts file, defaults to true'))
.addOption(new Option('--silent', 'suppress all output except errors').default(false))
.action(generateAction);

Expand Down
8 changes: 8 additions & 0 deletions packages/cli/src/plugins/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,19 @@ const plugin: CliPlugin = {
throw new Error('The "importWithFileExtension" option must be a string if specified.');
}

// whether to generate models.ts
const generateModelTypes = pluginOptions['generateModels'] !== false;

// whether to generate input.ts
const generateInputTypes = pluginOptions['generateInput'] !== false;

await new TsSchemaGenerator().generate(model, {
outDir,
lite,
liteOnly,
importWithFileExtension: importWithFileExtension as string | undefined,
generateModelTypes,
generateInputTypes,
});
},
};
Expand Down
144 changes: 144 additions & 0 deletions packages/cli/test/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,40 @@ describe('CLI generate command test', () => {
expect(fs.existsSync(path.join(workDir, 'bar/schema.ts'))).toBe(true);
});

it('should respect plugin lite options', async () => {
const modelWithPlugin = `
plugin typescript {
provider = "@core/typescript"
lite = true
}

model User {
id String @id @default(cuid())
}
`;
const { workDir } = await createProject(modelWithPlugin);
runCli('generate', workDir);
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true);
expect(fs.existsSync(path.join(workDir, 'zenstack/schema-lite.ts'))).toBe(true);
});

it('should respect plugin lite-only options', async () => {
const modelWithPlugin = `
plugin typescript {
provider = "@core/typescript"
liteOnly = true
}

model User {
id String @id @default(cuid())
}
`;
const { workDir } = await createProject(modelWithPlugin);
runCli('generate', workDir);
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(false);
expect(fs.existsSync(path.join(workDir, 'zenstack/schema-lite.ts'))).toBe(true);
});

it('should respect lite option', async () => {
const { workDir } = await createProject(model);
runCli('generate --lite', workDir);
Expand All @@ -73,4 +107,114 @@ describe('CLI generate command test', () => {
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(false);
expect(fs.existsSync(path.join(workDir, 'zenstack/schema-lite.ts'))).toBe(true);
});

it('should respect explicit liteOnly true option', async () => {
const { workDir } = await createProject(model);
runCli('generate --lite-only=true', workDir);
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(false);
expect(fs.existsSync(path.join(workDir, 'zenstack/schema-lite.ts'))).toBe(true);
});

it('should respect explicit liteOnly false option', async () => {
const { workDir } = await createProject(model);
runCli('generate --lite-only=false', workDir);
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true);
expect(fs.existsSync(path.join(workDir, 'zenstack/schema-lite.ts'))).toBe(false);
});

it('should prefer CLI options over @core/typescript plugin settings for lite and liteOnly', async () => {
const modelWithPlugin = `
plugin typescript {
provider = "@core/typescript"
lite = true
liteOnly = true
}

model User {
id String @id @default(cuid())
}
`;
const { workDir } = await createProject(modelWithPlugin);
runCli('generate --lite=false --lite-only=false', workDir);
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true);
expect(fs.existsSync(path.join(workDir, 'zenstack/schema-lite.ts'))).toBe(false);
});

it('should generate models.ts and input.ts by default', async () => {
const { workDir } = await createProject(model);
runCli('generate', workDir);
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true);
expect(fs.existsSync(path.join(workDir, 'zenstack/models.ts'))).toBe(true);
expect(fs.existsSync(path.join(workDir, 'zenstack/input.ts'))).toBe(true);
});

it('should respect plugin options for generateModels and generateInput by default', async () => {
const modelWithPlugin = `
plugin typescript {
provider = "@core/typescript"
generateModels = false
generateInput = false
}

model User {
id String @id @default(cuid())
}
`;
const { workDir } = await createProject(modelWithPlugin);
runCli('generate', workDir);
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true);
expect(fs.existsSync(path.join(workDir, 'zenstack/models.ts'))).toBe(false);
expect(fs.existsSync(path.join(workDir, 'zenstack/input.ts'))).toBe(false);
});

it('should generate models.ts when --generate-models=true is passed', async () => {
const { workDir } = await createProject(model);
runCli('generate --generate-models=true', workDir);
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true);
expect(fs.existsSync(path.join(workDir, 'zenstack/models.ts'))).toBe(true);
expect(fs.existsSync(path.join(workDir, 'zenstack/input.ts'))).toBe(true);
});

it('should not generate models.ts when --generate-models=false is passed', async () => {
const { workDir } = await createProject(model);
runCli('generate --generate-models=false', workDir);
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true);
expect(fs.existsSync(path.join(workDir, 'zenstack/models.ts'))).toBe(false);
expect(fs.existsSync(path.join(workDir, 'zenstack/input.ts'))).toBe(true);
});

it('should generate input.ts when --generate-input=true is passed', async () => {
const { workDir } = await createProject(model);
runCli('generate --generate-input=true', workDir);
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true);
expect(fs.existsSync(path.join(workDir, 'zenstack/models.ts'))).toBe(true);
expect(fs.existsSync(path.join(workDir, 'zenstack/input.ts'))).toBe(true);
});

it('should not generate input.ts when --generate-input=false is passed', async () => {
const { workDir } = await createProject(model);
runCli('generate --generate-input=false', workDir);
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true);
expect(fs.existsSync(path.join(workDir, 'zenstack/models.ts'))).toBe(true);
expect(fs.existsSync(path.join(workDir, 'zenstack/input.ts'))).toBe(false);
});

it('should prefer CLI options over @core/typescript plugin settings for generateModels and generateInput', async () => {
const modelWithPlugin = `
plugin typescript {
provider = "@core/typescript"
generateModels = false
generateInput = false
}

model User {
id String @id @default(cuid())
}
`;
const { workDir } = await createProject(modelWithPlugin);
runCli('generate --generate-models --generate-input', workDir);
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true);
expect(fs.existsSync(path.join(workDir, 'zenstack/models.ts'))).toBe(true);
expect(fs.existsSync(path.join(workDir, 'zenstack/input.ts'))).toBe(true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

/* eslint-disable */

import { type SchemaDef, ExpressionUtils } from "@zenstackhq/orm/schema";
import { type SchemaDef, ExpressionUtils } from "@zenstackhq/schema";
export class SchemaType implements SchemaDef {
provider = {
type: "sqlite"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// NOTE: Test fixture schema used for TanStack Query typing tests. //
//////////////////////////////////////////////////////////////////////////////////////////////

import { type SchemaDef, ExpressionUtils } from '@zenstackhq/orm/schema';
import { type SchemaDef, ExpressionUtils } from '@zenstackhq/schema';

export class SchemaType implements SchemaDef {
provider = {
Expand Down
16 changes: 10 additions & 6 deletions packages/orm/src/client/options.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { Dialect, Expression, ExpressionBuilder, KyselyConfig } from 'kysely';
import type { Dialect, Expression, ExpressionBuilder, KyselyConfig, OperandExpression } from 'kysely';
import type { GetModel, GetModelFields, GetModels, ProcedureDef, ScalarFields, SchemaDef } from '../schema';
import type { PrependParameter } from '../utils/type-utils';
import type { FilterPropertyToKind } from './constants';
import type { ClientContract, CRUD_EXT } from './contract';
import type { GetProcedureNames, ProcedureHandlerFunc } from './crud-types';
Expand Down Expand Up @@ -226,10 +225,15 @@ export type OmitConfig<Schema extends SchemaDef> = {

export type ComputedFieldsOptions<Schema extends SchemaDef> = {
[Model in GetModels<Schema> as 'computedFields' extends keyof GetModel<Schema, Model> ? Model : never]: {
[Field in keyof Schema['models'][Model]['computedFields']]: PrependParameter<
ExpressionBuilder<ToKyselySchema<Schema>, Model>,
Schema['models'][Model]['computedFields'][Field]
>;
[Field in keyof Schema['models'][Model]['computedFields']]: Schema['models'][Model]['computedFields'][Field] extends infer Func
? Func extends (...args: any[]) => infer R
? (
// inject a first parameter for expression builder
p: ExpressionBuilder<ToKyselySchema<Schema>, Model>,
...args: Parameters<Func>
) => OperandExpression<R> // wrap the return type with Kysely `OperandExpression`
: never
: never;
};
};

Expand Down
Loading
Loading