Skip to content

Commit ca40e00

Browse files
irvinzzymc9
andauthored
Truncate too long delegate aux relation name in runtime (#1629)
Co-authored-by: Yiming <[email protected]> Co-authored-by: ymc9 <[email protected]>
1 parent 179634e commit ca40e00

File tree

11 files changed

+156
-40
lines changed

11 files changed

+156
-40
lines changed

packages/runtime/src/cross/model-meta.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,11 @@ export type ModelMeta = {
161161
* Name of model that backs the `auth()` function
162162
*/
163163
authModel?: string;
164+
165+
/**
166+
* Optional map from full names to shortened names, used for extra fields/relations generated by ZenStack
167+
*/
168+
shortNameMap?: Record<string, string>;
164169
};
165170

166171
/**

packages/runtime/src/enhancements/delegate.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1040,7 +1040,10 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
10401040
}
10411041

10421042
private makeAuxRelationName(model: ModelInfo) {
1043-
return `${DELEGATE_AUX_RELATION_PREFIX}_${lowerCaseFirst(model.name)}`;
1043+
const name = `${DELEGATE_AUX_RELATION_PREFIX}_${lowerCaseFirst(model.name)}`;
1044+
// make sure we look up into short name map to see if it's truncated
1045+
const shortName = this.options.modelMeta.shortNameMap?.[name];
1046+
return shortName ?? name;
10441047
}
10451048

10461049
private getModelName(model: string) {

packages/schema/src/cli/plugin-runner.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
type OptionValue,
1515
type PluginDeclaredOptions,
1616
type PluginFunction,
17+
type PluginOptions,
1718
type PluginResult,
1819
} from '@zenstackhq/sdk';
1920
import { type DMMF } from '@zenstackhq/sdk/prisma';
@@ -131,18 +132,24 @@ export class PluginRunner {
131132

132133
// run core plugins first
133134
let dmmf: DMMF.Document | undefined = undefined;
135+
let shortNameMap: Map<string, string> | undefined;
134136
let prismaClientPath = '@prisma/client';
135137
const project = createProject();
136138
for (const { name, description, run, options: pluginOptions } of corePlugins) {
137139
const options = { ...pluginOptions, prismaClientPath };
138-
const r = await this.runPlugin(name, description, run, runnerOptions, options, dmmf, project);
140+
const r = await this.runPlugin(name, description, run, runnerOptions, options, dmmf, shortNameMap, project);
139141
warnings.push(...(r?.warnings ?? [])); // the null-check is for backward compatibility
140142

141143
if (r.dmmf) {
142144
// use the DMMF returned by the plugin
143145
dmmf = r.dmmf;
144146
}
145147

148+
if (r.shortNameMap) {
149+
// use the model short name map returned by the plugin
150+
shortNameMap = r.shortNameMap;
151+
}
152+
146153
if (r.prismaClientPath) {
147154
// use the prisma client path returned by the plugin
148155
prismaClientPath = r.prismaClientPath;
@@ -155,7 +162,7 @@ export class PluginRunner {
155162
// run user plugins
156163
for (const { name, description, run, options: pluginOptions } of userPlugins) {
157164
const options = { ...pluginOptions, prismaClientPath };
158-
const r = await this.runPlugin(name, description, run, runnerOptions, options, dmmf, project);
165+
const r = await this.runPlugin(name, description, run, runnerOptions, options, dmmf, shortNameMap, project);
159166
warnings.push(...(r?.warnings ?? [])); // the null-check is for backward compatibility
160167
}
161168

@@ -311,6 +318,7 @@ export class PluginRunner {
311318
runnerOptions: PluginRunnerOptions,
312319
options: PluginDeclaredOptions,
313320
dmmf: DMMF.Document | undefined,
321+
shortNameMap: Map<string, string> | undefined,
314322
project: Project
315323
) {
316324
const title = description ?? `Running plugin ${colors.cyan(name)}`;
@@ -325,7 +333,12 @@ export class PluginRunner {
325333
options,
326334
},
327335
async () => {
328-
return await run(runnerOptions.schema, { ...options, schemaPath: runnerOptions.schemaPath }, dmmf, {
336+
const finalOptions = {
337+
...options,
338+
schemaPath: runnerOptions.schemaPath,
339+
shortNameMap,
340+
} as PluginOptions;
341+
return await run(runnerOptions.schema, finalOptions, dmmf, {
329342
output: runnerOptions.output,
330343
compile: runnerOptions.compile,
331344
tsProject: project,

packages/schema/src/plugins/enhancer/model-meta/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ export async function generate(model: Model, options: PluginOptions, project: Pr
1313
output: outFile,
1414
generateAttributes: true,
1515
preserveTsFiles,
16+
shortNameMap: options.shortNameMap,
1617
});
1718
}

packages/schema/src/plugins/prisma/index.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import { PluginError, PluginFunction, getLiteral, resolvePath } from '@zenstackhq/sdk';
1+
import { PluginError, type PluginFunction, type PluginOptions, getLiteral, resolvePath } from '@zenstackhq/sdk';
22
import { GeneratorDecl, isGeneratorDecl } from '@zenstackhq/sdk/ast';
33
import { getDMMF } from '@zenstackhq/sdk/prisma';
4+
import colors from 'colors';
45
import fs from 'fs';
56
import path from 'path';
67
import stripColor from 'strip-color';
78
import telemetry from '../../telemetry';
89
import { execPackage } from '../../utils/exec-utils';
910
import { findUp } from '../../utils/pkg-utils';
1011
import { PrismaSchemaGenerator } from './schema-generator';
11-
import colors from 'colors';
1212

1313
export const name = 'Prisma';
1414
export const description = 'Generating Prisma schema';
@@ -19,7 +19,8 @@ const run: PluginFunction = async (model, options, _dmmf, _globalOptions) => {
1919
? resolvePath(options.output as string, options)
2020
: getDefaultPrismaOutputFile(options.schemaPath);
2121

22-
const warnings = await new PrismaSchemaGenerator(model).generate({ ...options, output });
22+
const mergedOptions = { ...options, output } as unknown as PluginOptions;
23+
const { warnings, shortNameMap } = await new PrismaSchemaGenerator(model).generate(mergedOptions);
2324
let prismaClientPath = '@prisma/client';
2425

2526
if (options.generateClient !== false) {
@@ -74,7 +75,7 @@ const run: PluginFunction = async (model, options, _dmmf, _globalOptions) => {
7475
datamodel: fs.readFileSync(output, 'utf-8'),
7576
});
7677

77-
return { warnings, dmmf, prismaClientPath };
78+
return { warnings, dmmf, prismaClientPath, shortNameMap };
7879
};
7980

8081
function getDefaultPrismaOutputFile(schemaPath: string) {

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

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ export class PrismaSchemaGenerator {
101101

102102
private mode: 'logical' | 'physical' = 'physical';
103103

104-
// a mapping from shortened names to their original full names
105-
private shortNameMap = new Map<string, string[]>();
104+
// a mapping from full names to shortened names
105+
private shortNameMap = new Map<string, string>();
106106

107107
constructor(private readonly zmodel: Model) {}
108108

@@ -160,7 +160,7 @@ export class PrismaSchemaGenerator {
160160
}
161161
}
162162

163-
return warnings;
163+
return { warnings, shortNameMap: this.shortNameMap };
164164
}
165165

166166
private generateDataSource(prisma: PrismaModel, dataSource: DataSource) {
@@ -318,7 +318,7 @@ export class PrismaSchemaGenerator {
318318

319319
// generate an optional relation field in delegate base model to each concrete model
320320
concreteModels.forEach((concrete) => {
321-
const auxName = `${DELEGATE_AUX_RELATION_PREFIX}_${this.truncate(lowerCaseFirst(concrete.name))}`;
321+
const auxName = this.truncate(`${DELEGATE_AUX_RELATION_PREFIX}_${lowerCaseFirst(concrete.name)}`);
322322
model.addField(auxName, new ModelFieldType(concrete.name, false, true));
323323
});
324324
}
@@ -339,7 +339,7 @@ export class PrismaSchemaGenerator {
339339
const idFields = getIdFields(base);
340340

341341
// add relation fields
342-
const relationField = `${DELEGATE_AUX_RELATION_PREFIX}_${this.truncate(lowerCaseFirst(base.name))}`;
342+
const relationField = this.truncate(`${DELEGATE_AUX_RELATION_PREFIX}_${lowerCaseFirst(base.name)}`);
343343
model.addField(relationField, base.name, [
344344
new PrismaFieldAttribute('@relation', [
345345
new PrismaAttributeArg(
@@ -403,9 +403,11 @@ export class PrismaSchemaGenerator {
403403
concreteModels.forEach((concrete) => {
404404
// aux relation name format: delegate_aux_[model]_[relationField]_[concrete]
405405
// e.g., delegate_aux_User_myAsset_Video
406-
const auxRelationName = `${dataModel.name}_${field.name}_${concrete.name}`;
406+
const auxRelationName = this.truncate(
407+
`${DELEGATE_AUX_RELATION_PREFIX}_${dataModel.name}_${field.name}_${concrete.name}`
408+
);
407409
const auxRelationField = model.addField(
408-
`${DELEGATE_AUX_RELATION_PREFIX}_${this.truncate(auxRelationName)}`,
410+
auxRelationName,
409411
new ModelFieldType(concrete.name, field.type.array, field.type.optional)
410412
);
411413

@@ -493,15 +495,15 @@ export class PrismaSchemaGenerator {
493495

494496
// fix its name
495497
const addedFkFieldName = `${dataModel.name}_${origForeignKey.name}_${concreteModel.name}`;
496-
addedFkField.name = `${DELEGATE_AUX_RELATION_PREFIX}_${this.truncate(addedFkFieldName)}`;
498+
addedFkField.name = this.truncate(`${DELEGATE_AUX_RELATION_PREFIX}_${addedFkFieldName}`);
497499

498500
// we also need to make sure `@unique` constraint's `map` parameter is fixed to avoid conflict
499501
const uniqueAttr = addedFkField.attributes.find(
500502
(attr) => (attr as PrismaFieldAttribute).name === '@unique'
501503
) as PrismaFieldAttribute;
502504
if (uniqueAttr) {
503505
const mapArg = uniqueAttr.args.find((arg) => arg.name === 'map');
504-
const constraintName = `${addedFkField.name}_unique`;
506+
const constraintName = this.truncate(`${addedFkField.name}_unique`);
505507
if (mapArg) {
506508
mapArg.value = new AttributeArgValue('String', constraintName);
507509
} else {
@@ -563,21 +565,29 @@ export class PrismaSchemaGenerator {
563565
return name;
564566
}
565567

566-
const shortName = name.slice(0, IDENTIFIER_NAME_MAX_LENGTH);
567-
const entry = this.shortNameMap.get(shortName);
568-
if (!entry) {
569-
this.shortNameMap.set(shortName, [name]);
570-
return `${shortName}_0`;
571-
} else {
572-
const index = entry.findIndex((n) => n === name);
573-
if (index >= 0) {
574-
return `${shortName}_${index}`;
575-
} else {
576-
const newIndex = entry.length;
577-
entry.push(name);
578-
return `${shortName}_${newIndex}`;
568+
const existing = this.shortNameMap.get(name);
569+
if (existing) {
570+
return existing;
571+
}
572+
573+
const baseName = name.slice(0, IDENTIFIER_NAME_MAX_LENGTH);
574+
let index = 0;
575+
let shortName = `${baseName}_${index}`;
576+
577+
// eslint-disable-next-line no-constant-condition
578+
while (true) {
579+
const conflict = Array.from(this.shortNameMap.values()).find((v) => v === shortName);
580+
if (!conflict) {
581+
this.shortNameMap.set(name, shortName);
582+
break;
579583
}
584+
585+
// try next index
586+
index++;
587+
shortName = `${baseName}_${index}`;
580588
}
589+
590+
return shortName;
581591
}
582592

583593
private nameRelationsInheritedFromDelegate(model: PrismaDataModel, decl: DataModel) {
@@ -626,7 +636,7 @@ export class PrismaSchemaGenerator {
626636
// relation name format: delegate_aux_[relationType]_[oppositeRelationField]_[concrete]
627637
const relAttr = getAttribute(f, '@relation');
628638
const name = `${fieldType.name}_${oppositeRelationField.name}_${decl.name}`;
629-
const relName = `${DELEGATE_AUX_RELATION_PREFIX}_${this.truncate(name)}`;
639+
const relName = this.truncate(`${DELEGATE_AUX_RELATION_PREFIX}_${name}`);
630640

631641
if (relAttr) {
632642
const nameArg = getAttributeArg(relAttr, 'name');

packages/sdk/src/model-meta-generator.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ export type ModelMetaGeneratorOptions = {
5454
* Whether to preserve the pre-compilation TypeScript files
5555
*/
5656
preserveTsFiles?: boolean;
57+
58+
/**
59+
* Map from full names to shortened names, used for extra fields/relations generated by ZenStack
60+
*/
61+
shortNameMap?: Map<string, string>;
5762
};
5863

5964
export async function generate(project: Project, models: DataModel[], options: ModelMetaGeneratorOptions) {
@@ -84,6 +89,7 @@ function generateModelMetadata(
8489
writeModels(sourceFile, writer, dataModels, options);
8590
writeDeleteCascade(writer, dataModels);
8691
writeAuthModel(writer, dataModels);
92+
writeShortNameMap(options, writer);
8793
});
8894
}
8995

@@ -125,6 +131,7 @@ function writeAuthModel(writer: CodeBlockWriter, dataModels: DataModel[]) {
125131
const authModel = getAuthModel(dataModels);
126132
if (authModel) {
127133
writer.writeLine(`authModel: '${authModel.name}'`);
134+
writer.writeLine(',');
128135
}
129136
}
130137

@@ -529,3 +536,15 @@ function isAutoIncrement(field: DataModelField) {
529536

530537
return isInvocationExpr(arg) && arg.function.$refText === 'autoincrement';
531538
}
539+
540+
function writeShortNameMap(options: ModelMetaGeneratorOptions, writer: CodeBlockWriter) {
541+
if (options.shortNameMap && options.shortNameMap.size > 0) {
542+
writer.write('shortNameMap:');
543+
writer.block(() => {
544+
for (const [key, value] of options.shortNameMap!) {
545+
writer.write(`${key}: '${value}',`);
546+
}
547+
});
548+
writer.write(',');
549+
}
550+
}

packages/sdk/src/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ export type PluginOptions = {
3030
* PrismaClient import path, either relative to `schemaPath` or absolute
3131
*/
3232
prismaClientPath?: string;
33+
34+
/**
35+
* An optional map of full names to shortened names
36+
* @private
37+
*/
38+
shortNameMap?: Map<string, string>;
3339
} & PluginDeclaredOptions;
3440

3541
/**
@@ -73,6 +79,12 @@ export type PluginResult = {
7379
* @private
7480
*/
7581
dmmf?: DMMF.Document;
82+
83+
/**
84+
* An optional map of full names to shortened names
85+
* @private
86+
*/
87+
shortNameMap?: Map<string, string>;
7688
};
7789

7890
/**

tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1196,6 +1196,50 @@ describe('Polymorphism Test', () => {
11961196
);
11971197
});
11981198

1199+
it('handles very long concrete model name', async () => {
1200+
const { db, user } = await setup();
1201+
1202+
await db.veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongModelNameA.create({
1203+
data: {
1204+
owner: { connect: { id: user.id } },
1205+
duration: 62,
1206+
url: 'https://whatever.com/example.mp4',
1207+
propA: 'propA',
1208+
},
1209+
});
1210+
1211+
await db.veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongModelNameB.create({
1212+
data: {
1213+
owner: { connect: { id: user.id } },
1214+
duration: 62,
1215+
url: 'https://whatever.com/example.mp4',
1216+
propB: 'propB',
1217+
},
1218+
});
1219+
1220+
const foundUser = await db.user.findFirst({
1221+
where: { id: user.id },
1222+
include: {
1223+
assets: true,
1224+
},
1225+
});
1226+
1227+
expect(foundUser).toEqual(
1228+
expect.objectContaining({
1229+
assets: expect.arrayContaining([
1230+
expect.objectContaining({
1231+
videoType: 'VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongModelNameA',
1232+
propA: 'propA',
1233+
}),
1234+
expect.objectContaining({
1235+
videoType: 'VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongModelNameB',
1236+
propB: 'propB',
1237+
}),
1238+
]),
1239+
})
1240+
);
1241+
});
1242+
11991243
it('typescript compilation plain prisma', async () => {
12001244
const src = `
12011245
import { PrismaClient } from '@prisma/client';

tests/integration/tests/enhancements/with-delegate/utils.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ model Gallery {
4444
id Int @id @default(autoincrement())
4545
images Image[]
4646
}
47+
48+
model VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongModelNameA extends Video {
49+
propA String
50+
}
51+
52+
model VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongModelNameB extends Video {
53+
propB String
54+
}
4755
`;
4856

4957
export const POLYMORPHIC_MANY_TO_MANY_SCHEMA = `

0 commit comments

Comments
 (0)