Skip to content

Commit

Permalink
Truncate too long delegate aux relation name in runtime (#1629)
Browse files Browse the repository at this point in the history
Co-authored-by: Yiming <[email protected]>
Co-authored-by: ymc9 <[email protected]>
  • Loading branch information
3 people authored Aug 5, 2024
1 parent 179634e commit ca40e00
Show file tree
Hide file tree
Showing 11 changed files with 156 additions and 40 deletions.
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 @@ -311,6 +318,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 @@ -325,7 +333,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
56 changes: 33 additions & 23 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 @@ -493,15 +495,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 @@ -563,21 +565,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 @@ -626,7 +636,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 @@ -125,6 +131,7 @@ function writeAuthModel(writer: CodeBlockWriter, dataModels: DataModel[]) {
const authModel = getAuthModel(dataModels);
if (authModel) {
writer.writeLine(`authModel: '${authModel.name}'`);
writer.writeLine(',');
}
}

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 && options.shortNameMap.size > 0) {
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
8 changes: 8 additions & 0 deletions tests/integration/tests/enhancements/with-delegate/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ model Gallery {
id Int @id @default(autoincrement())
images Image[]
}
model VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongModelNameA extends Video {
propA String
}
model VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongModelNameB extends Video {
propB String
}
`;

export const POLYMORPHIC_MANY_TO_MANY_SCHEMA = `
Expand Down
Loading

0 comments on commit ca40e00

Please sign in to comment.