Skip to content

Commit f8cc54d

Browse files
committed
feat: Custom property mapping
1 parent 8ebf3c2 commit f8cc54d

8 files changed

+116
-35
lines changed

src/generate-model/generate-model.spec.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import assert from 'assert';
22
import { expect } from 'chai';
33
import { Project, QuoteKind, SourceFile } from 'ts-morph';
44

5+
import { createConfig } from '../generate';
56
import { generateModel } from '../generate-model';
67
import { generatorOptions, stringContains } from '../testing';
8+
import { GeneratorConfiguration } from '../types';
79

810
describe('generate models', () => {
911
let sourceFile: SourceFile;
@@ -19,13 +21,20 @@ describe('generate models', () => {
1921
manipulationSettings: { quoteKind: QuoteKind.Single },
2022
});
2123
const {
24+
generator,
2225
prismaClientDmmf: {
2326
datamodel: { models },
2427
},
2528
} = await generatorOptions(schema, options);
2629
const [model] = models;
2730
sourceFile = project.createSourceFile('_.ts', sourceFileText);
28-
generateModel({ model, sourceFile, projectFilePath: () => '_.ts' });
31+
const config = createConfig(generator.config);
32+
generateModel({
33+
model,
34+
sourceFile,
35+
config,
36+
projectFilePath: () => '_.ts',
37+
});
2938
sourceText = sourceFile.getText();
3039
imports = sourceFile.getImportDeclarations().flatMap((d) =>
3140
d.getNamedImports().map((index) => ({

src/generate-model/generate-model.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,20 @@ import { SourceFile } from 'ts-morph';
33
import { generateClass } from '../generate-class';
44
import { generateImport } from '../generate-import';
55
import { generateProperty } from '../generate-property';
6-
import { PrismaDMMF } from '../types';
6+
import { GeneratorConfiguration, PrismaDMMF } from '../types';
77

88
type GenerateModelArgs = {
99
model: PrismaDMMF.Model;
1010
sourceFile: SourceFile;
1111
projectFilePath(data: { name: string; type: string }): string;
12+
config: GeneratorConfiguration;
1213
};
1314

1415
/**
1516
* Generate model (class).
1617
*/
1718
export function generateModel(args: GenerateModelArgs) {
18-
const { model, sourceFile, projectFilePath } = args;
19+
const { model, sourceFile, projectFilePath, config } = args;
1920
const classDeclaration = generateClass({
2021
decorator: {
2122
name: 'ObjectType',
@@ -33,6 +34,7 @@ export function generateModel(args: GenerateModelArgs) {
3334
className: model.name,
3435
classType: 'model',
3536
projectFilePath,
37+
config,
3638
});
3739
});
3840
}

src/generate-object.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,21 @@ import { SourceFile } from 'ts-morph';
33
import { generateClass } from './generate-class';
44
import { generateImport } from './generate-import';
55
import { generateProperty, Model } from './generate-property';
6+
import { GeneratorConfiguration } from './types';
67

78
type GenerateObjectArgs = {
89
sourceFile: SourceFile;
910
projectFilePath(data: { name: string; type: string }): string;
1011
model: Model;
1112
classType: string;
13+
config: GeneratorConfiguration;
1214
};
1315

1416
/**
1517
* Generate object type (class).
1618
*/
1719
export function generateObject(args: GenerateObjectArgs) {
18-
const { model, classType, sourceFile, projectFilePath } = args;
20+
const { model, classType, sourceFile, projectFilePath, config } = args;
1921
const classDeclaration = generateClass({
2022
decorator: {
2123
name: 'ObjectType',
@@ -32,6 +34,7 @@ export function generateObject(args: GenerateObjectArgs) {
3234
className: model.name,
3335
classType,
3436
projectFilePath,
37+
config,
3538
});
3639
});
3740
}

src/generate-property.ts

+22-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ClassDeclaration, SourceFile } from 'ts-morph';
33
import { generateClassProperty } from './generate-class';
44
import { generateDecorator } from './generate-decorator';
55
import { generateImport, generateProjectImport } from './generate-import';
6+
import { GeneratorConfiguration } from './types';
67
import { toGraphqlImportType, toPropertyType } from './utils';
78

89
export type Field = {
@@ -28,14 +29,33 @@ type GeneratePropertyArgs = {
2829
className: string;
2930
field: Field;
3031
classType: string;
32+
config: GeneratorConfiguration;
3133
};
3234

3335
/**
3436
* Generate property for class.
3537
*/
3638
export function generateProperty(args: GeneratePropertyArgs) {
37-
const { field, className, classDeclaration, sourceFile, projectFilePath, classType } = args;
38-
const propertyType = toPropertyType(field);
39+
const {
40+
field,
41+
className,
42+
classDeclaration,
43+
sourceFile,
44+
projectFilePath,
45+
classType,
46+
config,
47+
} = args;
48+
const customType = config.customPropertyTypes[field.type] as
49+
| { name: string; specifier: string }
50+
| undefined;
51+
if (customType) {
52+
generateImport({
53+
sourceFile,
54+
name: customType.name,
55+
moduleSpecifier: customType.specifier,
56+
});
57+
}
58+
const propertyType = customType?.name || toPropertyType(field);
3959
let fieldType = field.type;
4060
if (field.isId || field.kind === 'scalar') {
4161
fieldType = generateImport({

src/generate.spec.ts

+33-15
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import { PropertyDeclaration, SourceFile } from 'ts-morph';
44

55
import { generate } from './generate';
66
import { generatorOptions, getImportDeclarations, stringContains } from './testing';
7+
import { GeneratorConfigurationOptions } from './types';
78

89
describe('main generate', () => {
910
let property: PropertyDeclaration | undefined;
1011
let sourceFile: SourceFile | undefined;
1112
let sourceFiles: SourceFile[];
1213
let sourceText: string;
13-
async function getResult(args: { schema: string } & Record<string, any>) {
14+
async function getResult(args: { schema: string } & GeneratorConfigurationOptions) {
1415
const { schema, ...options } = args;
1516
const generateOptions = {
1617
...(await generatorOptions(schema, options)),
@@ -218,7 +219,7 @@ describe('main generate', () => {
218219

219220
it('get rid of atomic number operations', async () => {
220221
await getResult({
221-
atomicNumberOperations: false,
222+
atomicNumberOperations: 'false',
222223
schema: `
223224
model User {
224225
id String @id
@@ -278,7 +279,7 @@ describe('main generate', () => {
278279

279280
it('user args type', async () => {
280281
await getResult({
281-
atomicNumberOperations: false,
282+
atomicNumberOperations: 'false',
282283
schema: `
283284
model User {
284285
id String @id
@@ -326,12 +327,8 @@ describe('main generate', () => {
326327
decoratorArguments = struct.decorators?.[0].arguments;
327328
assert.strictEqual(decoratorArguments?.[0], '() => UserMaxAggregateInput');
328329

329-
const imports = sourceFile.getImportDeclarations().flatMap((d) =>
330-
d.getNamedImports().map((index) => ({
331-
name: index.getName(),
332-
specifier: d.getModuleSpecifierValue(),
333-
})),
334-
);
330+
const imports = getImportDeclarations(sourceFile);
331+
335332
assert(imports.find((x) => x.name === 'UserAvgAggregateInput'));
336333
assert(imports.find((x) => x.name === 'UserSumAggregateInput'));
337334
assert(imports.find((x) => x.name === 'UserMinAggregateInput'));
@@ -340,7 +337,7 @@ describe('main generate', () => {
340337

341338
it('aggregate output types', async () => {
342339
await getResult({
343-
atomicNumberOperations: false,
340+
atomicNumberOperations: 'false',
344341
schema: `
345342
model User {
346343
id String @id
@@ -376,7 +373,7 @@ describe('main generate', () => {
376373
date DateTime?
377374
}
378375
`,
379-
combineScalarFilters: false,
376+
combineScalarFilters: 'false',
380377
});
381378
const filePaths = sourceFiles.map((s) => String(s.getFilePath()));
382379
const userWhereInput = sourceFiles.find((s) =>
@@ -410,7 +407,7 @@ describe('main generate', () => {
410407
USER
411408
}
412409
`,
413-
combineScalarFilters: true,
410+
combineScalarFilters: 'true',
414411
});
415412
const filePaths = sourceFiles.map((s) => String(s.getFilePath()));
416413
for (const filePath of filePaths) {
@@ -445,13 +442,13 @@ describe('main generate', () => {
445442
USER
446443
}
447444
`,
448-
atomicNumberOperations: false,
445+
atomicNumberOperations: 'false',
449446
});
450447
expect(sourceFiles.length).to.be.greaterThan(0);
451448
for (const sourceFile of sourceFiles) {
452449
sourceFile.getClasses().forEach((classDeclaration) => {
453450
if (classDeclaration.getName()?.endsWith('FieldUpdateOperationsInput')) {
454-
assert.fail(`Class should not exists ${classDeclaration.getName()!}`);
451+
expect.fail(`Class should not exists ${classDeclaration.getName()!}`);
455452
}
456453
});
457454
}
@@ -469,8 +466,29 @@ describe('main generate', () => {
469466
}))
470467
.forEach((struct) => {
471468
if (struct.types.find((s) => s.endsWith('FieldUpdateOperationsInput'))) {
472-
expect.fail(`Property ${struct.name} typed ${struct.type}`);
469+
expect.fail(`Property ${struct.name} typed ${String(struct.type)}`);
473470
}
474471
});
475472
});
473+
474+
it('custom property mapping', async () => {
475+
await getResult({
476+
schema: `
477+
model User {
478+
id String @id
479+
d Decimal
480+
}
481+
`,
482+
customPropertyTypes: 'Decimal:MyDec:decimal.js',
483+
});
484+
const sourceFile = sourceFiles.find((s) => s.getFilePath().endsWith('user.model.ts'));
485+
assert(sourceFile);
486+
const property = sourceFile.getClasses()[0]?.getProperty('d')?.getStructure();
487+
expect(property?.type).to.equal('MyDec');
488+
const imports = getImportDeclarations(sourceFile);
489+
expect(imports).to.deep.contain({
490+
name: 'MyDec',
491+
specifier: 'decimal.js',
492+
});
493+
});
476494
});

src/generate.ts

+28-12
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { generateInput } from './generate-input';
1010
import { generateModel } from './generate-model';
1111
import { generateObject } from './generate-object';
1212
import { mutateFilters } from './mutate-filters';
13-
import { PrismaDMMF } from './types';
13+
import { GeneratorConfiguration, PrismaDMMF } from './types';
1414
import {
1515
featureName,
1616
getOutputTypeName,
@@ -19,15 +19,34 @@ import {
1919
} from './utils';
2020
import { generateFileName } from './utils/generate-file-name';
2121

22+
export function createConfig(data: Record<string, string | undefined>): GeneratorConfiguration {
23+
return {
24+
outputFilePattern: data.outputFilePattern || `{feature}/{dasherizedName}.{type}.ts`,
25+
combineScalarFilters: ['true', '1'].includes(data.combineScalarFilters ?? 'true'),
26+
atomicNumberOperations: ['true', '1'].includes(data.atomicNumberOperations ?? 'false'),
27+
customPropertyTypes: Object.fromEntries(
28+
(data.customPropertyTypes || '')
29+
.split(',')
30+
.map((s) => s.trim())
31+
.filter(Boolean)
32+
.map((kv) => kv.split(':'))
33+
.map(({ 0: key, 1: name, 2: specifier }) => [
34+
key,
35+
{ name: name || key, specifier },
36+
]),
37+
),
38+
};
39+
}
40+
2241
type GenerateArgs = GeneratorOptions & {
2342
prismaClientDmmf?: PrismaDMMF.Document;
2443
fileExistsSync?: typeof existsSync;
2544
};
2645

2746
export async function generate(args: GenerateArgs) {
2847
const { generator, otherGenerators } = args;
29-
const output = generator.output;
30-
assert(output, 'generator.output is empty');
48+
const config = createConfig(generator.config);
49+
assert(generator.output, 'generator.output is empty');
3150
const fileExistsSync = args.fileExistsSync ?? existsSync;
3251
const prismaClientOutput = otherGenerators.find((x) => x.provider === 'prisma-client-js')
3352
?.output;
@@ -43,7 +62,7 @@ export async function generate(args: GenerateArgs) {
4362
const projectFilePath = (args: { name: string; type: string; feature?: string }) => {
4463
return generateFileName({
4564
...args,
46-
template: generator.config.outputFilePattern,
65+
template: config.outputFilePattern,
4766
models,
4867
});
4968
};
@@ -55,7 +74,7 @@ export async function generate(args: GenerateArgs) {
5574
return sourceFile;
5675
}
5776
let sourceFileText = '';
58-
const localFilePath = path.join(output, filePath);
77+
const localFilePath = path.join(generator.output!, filePath);
5978
if (fileExistsSync(localFilePath)) {
6079
sourceFileText = await fs.readFile(localFilePath, { encoding: 'utf8' });
6180
}
@@ -78,18 +97,14 @@ export async function generate(args: GenerateArgs) {
7897
// Generate models
7998
for (const model of prismaClientDmmf.datamodel.models) {
8099
const sourceFile = await createSourceFile({ type: 'model', name: model.name });
81-
generateModel({ model, sourceFile, projectFilePath });
100+
generateModel({ model, sourceFile, projectFilePath, config });
82101
}
83102
// Generate inputs
84103
let inputTypes = prismaClientDmmf.schema.inputTypes;
85104
inputTypes = inputTypes.filter(
86105
mutateFilters(inputTypes, {
87-
combineScalarFilters: JSON.parse(
88-
(generator.config.combineScalarFilters as string | undefined) ?? 'true',
89-
) as boolean,
90-
atomicNumberOperations: JSON.parse(
91-
(generator.config.atomicNumberOperations as string | undefined) ?? 'false',
92-
) as boolean,
106+
combineScalarFilters: config.combineScalarFilters,
107+
atomicNumberOperations: config.atomicNumberOperations,
93108
}),
94109
);
95110
// Create aggregate inputs
@@ -149,6 +164,7 @@ export async function generate(args: GenerateArgs) {
149164
sourceFile,
150165
projectFilePath,
151166
model,
167+
config,
152168
});
153169
}
154170

src/testing/generator-options.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import crypto from 'crypto';
44
import findCacheDir from 'find-cache-dir';
55
import fs from 'fs';
66

7-
import { PrismaDMMF } from '../types';
7+
import { GeneratorConfigurationOptions, PrismaDMMF } from '../types';
88

99
const {
1010
dependencies: { '@prisma/generator-helper': generatorVersion },
@@ -17,7 +17,7 @@ const cachePath: string = findCacheDir({ name: 'createGeneratorOptions', create:
1717
*/
1818
export async function generatorOptions(
1919
schema: string,
20-
options?: Record<string, any>,
20+
options?: GeneratorConfigurationOptions,
2121
): Promise<GeneratorOptions & { prismaClientDmmf: PrismaDMMF.Document }> {
2222
// eslint-disable-next-line prefer-rest-params
2323
const data = JSON.stringify([generatorVersion, arguments]);
@@ -32,6 +32,7 @@ export async function generatorOptions(
3232
}
3333
generator client {
3434
provider = "prisma-client-js"
35+
previewFeatures = ["nativeTypes"]
3536
}
3637
generator proxy {
3738
provider = "node -r ts-node/register/transpile-only src/testing/proxy-generator.ts"
@@ -40,6 +41,7 @@ export async function generatorOptions(
4041
outputFilePattern = "${options?.outputFilePattern || ''}"
4142
combineScalarFilters = ${JSON.stringify(options?.combineScalarFilters ?? true)}
4243
atomicNumberOperations = ${JSON.stringify(options?.atomicNumberOperations ?? false)}
44+
customPropertyTypes = "${options?.customPropertyTypes || ''}"
4345
}
4446
${schema}
4547
`;

0 commit comments

Comments
 (0)