Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Truncate too long delegate aux relation name in runtime #1629

Merged
merged 9 commits into from
Aug 5, 2024
5 changes: 5 additions & 0 deletions packages/runtime/src/cross/model-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,11 @@ export type ModelMeta = {
* Name of model that backs the `auth()` function
*/
authModel?: string;

/**
* Optional map from full names to shortened names, used for extra fields/relations generated by ZenStack
*/
shortNameMap?: Record<string, string>;
};

/**
Expand Down
5 changes: 4 additions & 1 deletion packages/runtime/src/enhancements/delegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1040,7 +1040,10 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
}

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

private getModelName(model: string) {
Expand Down
19 changes: 16 additions & 3 deletions packages/schema/src/cli/plugin-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
type OptionValue,
type PluginDeclaredOptions,
type PluginFunction,
type PluginOptions,
type PluginResult,
} from '@zenstackhq/sdk';
import { type DMMF } from '@zenstackhq/sdk/prisma';
Expand Down Expand Up @@ -131,18 +132,24 @@ export class PluginRunner {

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

if (r.dmmf) {
// use the DMMF returned by the plugin
dmmf = r.dmmf;
}

if (r.shortNameMap) {
// use the model short name map returned by the plugin
shortNameMap = r.shortNameMap;
}

if (r.prismaClientPath) {
// use the prisma client path returned by the plugin
prismaClientPath = r.prismaClientPath;
Expand All @@ -155,7 +162,7 @@ export class PluginRunner {
// run user plugins
for (const { name, description, run, options: pluginOptions } of userPlugins) {
const options = { ...pluginOptions, prismaClientPath };
const r = await this.runPlugin(name, description, run, runnerOptions, options, dmmf, project);
const r = await this.runPlugin(name, description, run, runnerOptions, options, dmmf, shortNameMap, project);
warnings.push(...(r?.warnings ?? [])); // the null-check is for backward compatibility
}

Expand Down Expand Up @@ -308,6 +315,7 @@ export class PluginRunner {
runnerOptions: PluginRunnerOptions,
options: PluginDeclaredOptions,
dmmf: DMMF.Document | undefined,
shortNameMap: Map<string, string> | undefined,
project: Project
) {
const title = description ?? `Running plugin ${colors.cyan(name)}`;
Expand All @@ -322,7 +330,12 @@ export class PluginRunner {
options,
},
async () => {
return await run(runnerOptions.schema, { ...options, schemaPath: runnerOptions.schemaPath }, dmmf, {
const finalOptions = {
...options,
schemaPath: runnerOptions.schemaPath,
shortNameMap,
} as PluginOptions;
return await run(runnerOptions.schema, finalOptions, dmmf, {
output: runnerOptions.output,
compile: runnerOptions.compile,
tsProject: project,
Expand Down
1 change: 1 addition & 0 deletions packages/schema/src/plugins/enhancer/model-meta/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ export async function generate(model: Model, options: PluginOptions, project: Pr
output: outFile,
generateAttributes: true,
preserveTsFiles,
shortNameMap: options.shortNameMap,
});
}
9 changes: 5 additions & 4 deletions packages/schema/src/plugins/prisma/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { PluginError, PluginFunction, getLiteral, resolvePath } from '@zenstackhq/sdk';
import { PluginError, type PluginFunction, type PluginOptions, getLiteral, resolvePath } from '@zenstackhq/sdk';
import { GeneratorDecl, isGeneratorDecl } from '@zenstackhq/sdk/ast';
import { getDMMF } from '@zenstackhq/sdk/prisma';
import colors from 'colors';
import fs from 'fs';
import path from 'path';
import stripColor from 'strip-color';
import telemetry from '../../telemetry';
import { execPackage } from '../../utils/exec-utils';
import { findUp } from '../../utils/pkg-utils';
import { PrismaSchemaGenerator } from './schema-generator';
import colors from 'colors';

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

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

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

return { warnings, dmmf, prismaClientPath };
return { warnings, dmmf, prismaClientPath, shortNameMap };
};

function getDefaultPrismaOutputFile(schemaPath: string) {
Expand Down
61 changes: 37 additions & 24 deletions packages/schema/src/plugins/prisma/schema-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ export class PrismaSchemaGenerator {

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

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

constructor(private readonly zmodel: Model) {}

Expand Down Expand Up @@ -160,7 +160,7 @@ export class PrismaSchemaGenerator {
}
}

return warnings;
return { warnings, shortNameMap: this.shortNameMap };
}

private generateDataSource(prisma: PrismaModel, dataSource: DataSource) {
Expand Down Expand Up @@ -318,7 +318,7 @@ export class PrismaSchemaGenerator {

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

// add relation fields
const relationField = `${DELEGATE_AUX_RELATION_PREFIX}_${this.truncate(lowerCaseFirst(base.name))}`;
const relationField = this.truncate(`${DELEGATE_AUX_RELATION_PREFIX}_${lowerCaseFirst(base.name)}`);
model.addField(relationField, base.name, [
new PrismaFieldAttribute('@relation', [
new PrismaAttributeArg(
Expand Down Expand Up @@ -403,9 +403,11 @@ export class PrismaSchemaGenerator {
concreteModels.forEach((concrete) => {
// aux relation name format: delegate_aux_[model]_[relationField]_[concrete]
// e.g., delegate_aux_User_myAsset_Video
const auxRelationName = `${dataModel.name}_${field.name}_${concrete.name}`;
const auxRelationName = this.truncate(
`${DELEGATE_AUX_RELATION_PREFIX}_${dataModel.name}_${field.name}_${concrete.name}`
);
const auxRelationField = model.addField(
`${DELEGATE_AUX_RELATION_PREFIX}_${this.truncate(auxRelationName)}`,
auxRelationName,
new ModelFieldType(concrete.name, field.type.array, field.type.optional)
);

Expand Down Expand Up @@ -442,7 +444,10 @@ export class PrismaSchemaGenerator {

const addedRel = new PrismaFieldAttribute('@relation', [
// use field name as relation name for disambiguation
new PrismaAttributeArg(undefined, new AttributeArgValue('String', nameArg?.value || auxRelationField.name)),
new PrismaAttributeArg(
undefined,
new AttributeArgValue('String', nameArg?.value || auxRelationField.name)
),
new PrismaAttributeArg('fields', fieldsArg),
new PrismaAttributeArg('references', referencesArg),
]);
Expand Down Expand Up @@ -487,15 +492,15 @@ export class PrismaSchemaGenerator {

// fix its name
const addedFkFieldName = `${dataModel.name}_${origForeignKey.name}_${concreteModel.name}`;
addedFkField.name = `${DELEGATE_AUX_RELATION_PREFIX}_${this.truncate(addedFkFieldName)}`;
addedFkField.name = this.truncate(`${DELEGATE_AUX_RELATION_PREFIX}_${addedFkFieldName}`);

// we also need to make sure `@unique` constraint's `map` parameter is fixed to avoid conflict
const uniqueAttr = addedFkField.attributes.find(
(attr) => (attr as PrismaFieldAttribute).name === '@unique'
) as PrismaFieldAttribute;
if (uniqueAttr) {
const mapArg = uniqueAttr.args.find((arg) => arg.name === 'map');
const constraintName = `${addedFkField.name}_unique`;
const constraintName = this.truncate(`${addedFkField.name}_unique`);
if (mapArg) {
mapArg.value = new AttributeArgValue('String', constraintName);
} else {
Expand Down Expand Up @@ -557,21 +562,29 @@ export class PrismaSchemaGenerator {
return name;
}

const shortName = name.slice(0, IDENTIFIER_NAME_MAX_LENGTH);
const entry = this.shortNameMap.get(shortName);
if (!entry) {
this.shortNameMap.set(shortName, [name]);
return `${shortName}_0`;
} else {
const index = entry.findIndex((n) => n === name);
if (index >= 0) {
return `${shortName}_${index}`;
} else {
const newIndex = entry.length;
entry.push(name);
return `${shortName}_${newIndex}`;
const existing = this.shortNameMap.get(name);
if (existing) {
return existing;
}

const baseName = name.slice(0, IDENTIFIER_NAME_MAX_LENGTH);
let index = 0;
let shortName = `${baseName}_${index}`;

// eslint-disable-next-line no-constant-condition
while (true) {
const conflict = Array.from(this.shortNameMap.values()).find((v) => v === shortName);
if (!conflict) {
this.shortNameMap.set(name, shortName);
break;
}

// try next index
index++;
shortName = `${baseName}_${index}`;
}

return shortName;
}

private nameRelationsInheritedFromDelegate(model: PrismaDataModel, decl: DataModel) {
Expand Down Expand Up @@ -620,7 +633,7 @@ export class PrismaSchemaGenerator {
// relation name format: delegate_aux_[relationType]_[oppositeRelationField]_[concrete]
const relAttr = getAttribute(f, '@relation');
const name = `${fieldType.name}_${oppositeRelationField.name}_${decl.name}`;
const relName = `${DELEGATE_AUX_RELATION_PREFIX}_${this.truncate(name)}`;
const relName = this.truncate(`${DELEGATE_AUX_RELATION_PREFIX}_${name}`);

if (relAttr) {
const nameArg = getAttributeArg(relAttr, 'name');
Expand Down
19 changes: 19 additions & 0 deletions packages/sdk/src/model-meta-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ export type ModelMetaGeneratorOptions = {
* Whether to preserve the pre-compilation TypeScript files
*/
preserveTsFiles?: boolean;

/**
* Map from full names to shortened names, used for extra fields/relations generated by ZenStack
*/
shortNameMap?: Map<string, string>;
};

export async function generate(project: Project, models: DataModel[], options: ModelMetaGeneratorOptions) {
Expand Down Expand Up @@ -84,6 +89,7 @@ function generateModelMetadata(
writeModels(sourceFile, writer, dataModels, options);
writeDeleteCascade(writer, dataModels);
writeAuthModel(writer, dataModels);
writeShortNameMap(options, writer);
});
}

Expand Down Expand Up @@ -126,6 +132,7 @@ function writeAuthModel(writer: CodeBlockWriter, dataModels: DataModel[]) {
if (authModel) {
writer.writeLine(`authModel: '${authModel.name}'`);
}
writer.writeLine(',');
}

function writeDeleteCascade(writer: CodeBlockWriter, dataModels: DataModel[]) {
Expand Down Expand Up @@ -529,3 +536,15 @@ function isAutoIncrement(field: DataModelField) {

return isInvocationExpr(arg) && arg.function.$refText === 'autoincrement';
}

function writeShortNameMap(options: ModelMetaGeneratorOptions, writer: CodeBlockWriter) {
if (options.shortNameMap) {
writer.write('shortNameMap:');
writer.block(() => {
for (const [key, value] of options.shortNameMap!) {
writer.write(`${key}: '${value}',`);
}
});
writer.write(',');
}
}
12 changes: 12 additions & 0 deletions packages/sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ export type PluginOptions = {
* PrismaClient import path, either relative to `schemaPath` or absolute
*/
prismaClientPath?: string;

/**
* An optional map of full names to shortened names
* @private
*/
shortNameMap?: Map<string, string>;
} & PluginDeclaredOptions;

/**
Expand Down Expand Up @@ -73,6 +79,12 @@ export type PluginResult = {
* @private
*/
dmmf?: DMMF.Document;

/**
* An optional map of full names to shortened names
* @private
*/
shortNameMap?: Map<string, string>;
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1196,6 +1196,50 @@ describe('Polymorphism Test', () => {
);
});

it('handles very long concrete model name', async () => {
const { db, user } = await setup();

await db.veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongModelNameA.create({
data: {
owner: { connect: { id: user.id } },
duration: 62,
url: 'https://whatever.com/example.mp4',
propA: 'propA',
},
});

await db.veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongModelNameB.create({
data: {
owner: { connect: { id: user.id } },
duration: 62,
url: 'https://whatever.com/example.mp4',
propB: 'propB',
},
});

const foundUser = await db.user.findFirst({
where: { id: user.id },
include: {
assets: true,
},
});

expect(foundUser).toEqual(
expect.objectContaining({
assets: expect.arrayContaining([
expect.objectContaining({
videoType: 'VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongModelNameA',
propA: 'propA',
}),
expect.objectContaining({
videoType: 'VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongModelNameB',
propB: 'propB',
}),
]),
})
);
});

it('typescript compilation plain prisma', async () => {
const src = `
import { PrismaClient } from '@prisma/client';
Expand Down
Loading
Loading