Skip to content

Commit 6122e9b

Browse files
committed
fix(zmodel): enum is resolved to wrong element after merging base models
1 parent 273c107 commit 6122e9b

File tree

16 files changed

+169
-55
lines changed

16 files changed

+169
-55
lines changed

packages/schema/src/cli/cli-util.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { URI } from 'vscode-uri';
1212
import { PLUGIN_MODULE_NAME, STD_LIB_MODULE_NAME } from '../language-server/constants';
1313
import { ZModelFormatter } from '../language-server/zmodel-formatter';
1414
import { createZModelServices, ZModelServices } from '../language-server/zmodel-module';
15-
import { mergeBaseModel, resolveImport, resolveTransitiveImports } from '../utils/ast-utils';
15+
import { mergeBaseModels, resolveImport, resolveTransitiveImports } from '../utils/ast-utils';
1616
import { findUp } from '../utils/pkg-utils';
1717
import { getVersion } from '../utils/version-utils';
1818
import { CliError } from './cli-error';
@@ -101,10 +101,10 @@ export async function loadDocument(fileName: string): Promise<Model> {
101101
imported.map((m) => m.$document!.uri)
102102
);
103103

104-
validationAfterMerge(model);
104+
validationAfterImportMerge(model);
105105

106106
// merge fields and attributes from base models
107-
mergeBaseModel(model, services.references.Linker);
107+
mergeBaseModels(model, services.references.Linker);
108108

109109
// finally relink all references
110110
const relinkedModel = await relinkAll(model, services);
@@ -113,7 +113,7 @@ export async function loadDocument(fileName: string): Promise<Model> {
113113
}
114114

115115
// check global unique thing after merge imports
116-
function validationAfterMerge(model: Model) {
116+
function validationAfterImportMerge(model: Model) {
117117
const dataSources = model.declarations.filter((d) => isDataSource(d));
118118
if (dataSources.length == 0) {
119119
console.error(colors.red('Validation error: Model must define a datasource'));

packages/schema/src/language-server/validator/datamodel-validator.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@ import {
22
ArrayExpr,
33
DataModel,
44
DataModelField,
5-
isDataModel,
6-
isStringLiteral,
75
ReferenceExpr,
6+
isDataModel,
87
isEnum,
8+
isStringLiteral,
99
} from '@zenstackhq/language/ast';
10-
import { getLiteral, getModelIdFields, getModelUniqueFields, isDelegateModel } from '@zenstackhq/sdk';
11-
import { AstNode, DiagnosticInfo, getDocument, ValidationAcceptor } from 'langium';
12-
import { getModelFieldsWithBases } from '../../utils/ast-utils';
10+
import {
11+
getLiteral,
12+
getModelFieldsWithBases,
13+
getModelIdFields,
14+
getModelUniqueFields,
15+
isDelegateModel,
16+
} from '@zenstackhq/sdk';
17+
import { AstNode, DiagnosticInfo, ValidationAcceptor, getDocument } from 'langium';
1318
import { IssueCodes, SCALAR_TYPES } from '../constants';
1419
import { AstValidator } from '../types';
1520
import { getUniqueFields } from '../utils';

packages/schema/src/language-server/zmodel-code-action.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import {
1010
getDocument,
1111
} from 'langium';
1212

13+
import { getModelFieldsWithBases } from '@zenstackhq/sdk';
1314
import { CodeAction, CodeActionKind, CodeActionParams, Command, Diagnostic } from 'vscode-languageserver';
14-
import { getModelFieldsWithBases } from '../utils/ast-utils';
1515
import { IssueCodes } from './constants';
1616
import { MissingOppositeRelationData } from './validator/datamodel-validator';
1717
import { ZModelFormatter } from './zmodel-formatter';

packages/schema/src/utils/ast-utils.ts

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
ModelImport,
1818
ReferenceExpr,
1919
} from '@zenstackhq/language/ast';
20-
import { isDelegateModel, isFromStdlib } from '@zenstackhq/sdk';
20+
import { getModelFieldsWithBases, getRecursiveBases, isDelegateModel, isFromStdlib } from '@zenstackhq/sdk';
2121
import {
2222
AstNode,
2323
copyAstNode,
@@ -47,16 +47,13 @@ type BuildReference = (
4747
refText: string
4848
) => Reference<AstNode>;
4949

50-
export function mergeBaseModel(model: Model, linker: Linker) {
50+
export function mergeBaseModels(model: Model, linker: Linker) {
5151
const buildReference = linker.buildReference.bind(linker);
5252

53-
model.declarations.filter(isDataModel).forEach((decl) => {
54-
const dataModel = decl as DataModel;
55-
53+
model.declarations.filter(isDataModel).forEach((dataModel) => {
5654
const bases = getRecursiveBases(dataModel).reverse();
5755
if (bases.length > 0) {
5856
dataModel.fields = bases
59-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6057
.flatMap((base) => base.fields)
6158
// don't inherit skip-level fields
6259
.filter((f) => !f.$inheritedFrom)
@@ -67,16 +64,25 @@ export function mergeBaseModel(model: Model, linker: Linker) {
6764
.flatMap((base) => base.attributes.filter((attr) => filterBaseAttribute(base, attr)))
6865
.map((attr) => cloneAst(attr, dataModel, buildReference))
6966
.concat(dataModel.attributes);
70-
71-
// fix $containerIndex
72-
linkContentToContainer(dataModel);
7367
}
7468

69+
// mark base merged
7570
dataModel.$baseMerged = true;
7671
});
7772

7873
// remove abstract models
7974
model.declarations = model.declarations.filter((x) => !(isDataModel(x) && x.isAbstract));
75+
76+
model.declarations.filter(isDataModel).forEach((dm) => {
77+
// remove abstract super types
78+
dm.superTypes = dm.superTypes.filter((t) => t.ref && isDelegateModel(t.ref));
79+
80+
// fix $containerIndex
81+
linkContentToContainer(dm);
82+
});
83+
84+
// fix $containerIndex after deleting abstract models
85+
linkContentToContainer(model);
8086
}
8187

8288
function filterBaseAttribute(base: DataModel, attr: DataModelAttribute) {
@@ -244,29 +250,6 @@ export function getContainingDataModel(node: Expression): DataModel | undefined
244250
return undefined;
245251
}
246252

247-
export function getModelFieldsWithBases(model: DataModel, includeDelegate = true) {
248-
if (model.$baseMerged) {
249-
return model.fields;
250-
} else {
251-
return [...model.fields, ...getRecursiveBases(model, includeDelegate).flatMap((base) => base.fields)];
252-
}
253-
}
254-
255-
export function getRecursiveBases(dataModel: DataModel, includeDelegate = true): DataModel[] {
256-
const result: DataModel[] = [];
257-
dataModel.superTypes.forEach((superType) => {
258-
const baseDecl = superType.ref;
259-
if (baseDecl) {
260-
if (!includeDelegate && isDelegateModel(baseDecl)) {
261-
return;
262-
}
263-
result.push(baseDecl);
264-
result.push(...getRecursiveBases(baseDecl));
265-
}
266-
});
267-
return result;
268-
}
269-
270253
/**
271254
* Walk upward from the current AST node to find the first node that satisfies the predicate.
272255
*/

packages/schema/tests/utils.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as path from 'path';
55
import * as tmp from 'tmp';
66
import { URI } from 'vscode-uri';
77
import { createZModelServices } from '../src/language-server/zmodel-module';
8-
import { mergeBaseModel } from '../src/utils/ast-utils';
8+
import { mergeBaseModels } from '../src/utils/ast-utils';
99

1010
tmp.setGracefulCleanup();
1111

@@ -68,7 +68,7 @@ export async function loadModel(content: string, validate = true, verbose = true
6868
const model = (await doc.parseResult.value) as Model;
6969

7070
if (mergeBase) {
71-
mergeBaseModel(model, ZModel.references.Linker);
71+
mergeBaseModels(model, ZModel.references.Linker);
7272
}
7373

7474
return model;
@@ -87,13 +87,13 @@ export async function loadModelWithError(content: string, verbose = false) {
8787
}
8888

8989
export async function safelyLoadModel(content: string, validate = true, verbose = false) {
90-
const [ result ] = await Promise.allSettled([ loadModel(content, validate, verbose) ]);
90+
const [result] = await Promise.allSettled([loadModel(content, validate, verbose)]);
9191

92-
return result
92+
return result;
9393
}
9494

9595
export const errorLike = (msg: string) => ({
9696
reason: {
97-
message: expect.stringContaining(msg)
97+
message: expect.stringContaining(msg),
9898
},
99-
})
99+
});

packages/sdk/src/utils.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -504,17 +504,24 @@ export function getDataModelFieldReference(expr: Expression): DataModelField | u
504504
}
505505
}
506506

507-
export function getModelFieldsWithBases(model: DataModel) {
508-
return [...model.fields, ...getRecursiveBases(model).flatMap((base) => base.fields)];
507+
export function getModelFieldsWithBases(model: DataModel, includeDelegate = true) {
508+
if (model.$baseMerged) {
509+
return model.fields;
510+
} else {
511+
return [...model.fields, ...getRecursiveBases(model, includeDelegate).flatMap((base) => base.fields)];
512+
}
509513
}
510514

511-
export function getRecursiveBases(dataModel: DataModel): DataModel[] {
515+
export function getRecursiveBases(dataModel: DataModel, includeDelegate = true): DataModel[] {
512516
const result: DataModel[] = [];
513517
dataModel.superTypes.forEach((superType) => {
514518
const baseDecl = superType.ref;
515519
if (baseDecl) {
520+
if (!includeDelegate && isDelegateModel(baseDecl)) {
521+
return;
522+
}
516523
result.push(baseDecl);
517-
result.push(...getRecursiveBases(baseDecl));
524+
result.push(...getRecursiveBases(baseDecl, includeDelegate));
518525
}
519526
});
520527
return result;

packages/testtools/src/model.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as path from 'path';
55
import * as tmp from 'tmp';
66
import { URI } from 'vscode-uri';
77
import { createZModelServices } from 'zenstack/language-server/zmodel-module';
8-
import { mergeBaseModel } from 'zenstack/utils/ast-utils';
8+
import { mergeBaseModels } from 'zenstack/utils/ast-utils';
99

1010
tmp.setGracefulCleanup();
1111

@@ -53,7 +53,7 @@ export async function loadModel(content: string, validate = true, verbose = true
5353

5454
const model = (await doc.parseResult.value) as Model;
5555

56-
mergeBaseModel(model, ZModel.references.Linker);
56+
mergeBaseModels(model, ZModel.references.Linker);
5757

5858
return model;
5959
}

tests/integration/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@
88
"skipLibCheck": true,
99
"experimentalDecorators": true
1010
},
11-
"include": ["**/*.ts", "**/*.d.ts", "../regression/tests/issue-177.test.ts", "../regression/tests/issue-416.test.ts", "../regression/tests/issue-646.test.ts", "../regression/tests/issue-657.test.ts", "../regression/tests/issue-665.test.ts", "../regression/tests/issue-674.test.ts", "../regression/tests/issue-689.test.ts", "../regression/tests/issue-703.test.ts", "../regression/tests/issue-714.test.ts", "../regression/tests/issue-724.test.ts", "../regression/tests/issue-735.test.ts", "../regression/tests/issue-744.test.ts", "../regression/tests/issue-756.test.ts", "../regression/tests/issue-764.test.ts", "../regression/tests/issue-765.test.ts", "../regression/tests/issue-804.test.ts", "../regression/tests/issue-811.test.ts", "../regression/tests/issue-814.test.ts", "../regression/tests/issue-825.test.ts", "../regression/tests/issue-864.test.ts", "../regression/tests/issue-886.test.ts", "../regression/tests/issue-925.test.ts", "../regression/tests/issue-947.test.ts", "../regression/tests/issue-961.test.ts", "../regression/tests/issue-965.test.ts", "../regression/tests/issue-971.test.ts", "../regression/tests/issue-992.test.ts", "../regression/tests/issue-1014.test.ts", "../regression/tests/issue-1078.test.ts", "../regression/tests/issue-1080.test.ts", "../regression/tests/issue-1129.test.ts", "../regression/tests/issue-1167.test.ts", "../regression/tests/issue-1179.test.ts", "../regression/tests/issue-1186.test.ts", "../regression/tests/issue-1210.test.ts", "../regression/tests/issue-1235.test.ts", "../regression/tests/issue-1241.test.ts", "../regression/tests/issue-1257.test.ts", "../regression/tests/issue-1265.test.ts", "../regression/tests/issues.test.ts"]
11+
"include": ["tests/**/*.ts"]
1212
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { createPostgresDb, dropPostgresDb, loadSchema } from '@zenstackhq/testtools';
2+
3+
describe('issue 1435', () => {
4+
it('regression', async () => {
5+
let prisma: any;
6+
let dbUrl: string;
7+
8+
try {
9+
dbUrl = await createPostgresDb('issue-1435');
10+
const r = await loadSchema(
11+
`
12+
/* Interfaces */
13+
abstract model IBase {
14+
updatedAt DateTime @updatedAt
15+
createdAt DateTime @default(now())
16+
}
17+
18+
abstract model IAuth extends IBase {
19+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
20+
userId String @unique
21+
22+
@@allow('create', true)
23+
@@allow('all', auth() == user)
24+
}
25+
26+
abstract model IIntegration extends IBase {
27+
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
28+
organizationId String @unique
29+
30+
@@allow('all', organization.members?[user == auth() && type == OWNER])
31+
@@allow('read', organization.members?[user == auth()])
32+
}
33+
34+
/* Auth Stuff */
35+
model User extends IBase {
36+
id String @id @default(cuid())
37+
firstName String
38+
lastName String
39+
google GoogleAuth?
40+
memberships Member[]
41+
42+
@@allow('create', true)
43+
@@allow('all', auth() == this)
44+
}
45+
46+
model GoogleAuth extends IAuth {
47+
reference String @id
48+
refreshToken String
49+
}
50+
51+
/* Org Stuff */
52+
enum MemberType {
53+
OWNER
54+
MEMBER
55+
}
56+
57+
model Organization extends IBase {
58+
id String @id @default(cuid())
59+
name String
60+
members Member[]
61+
google GoogleIntegration?
62+
63+
@@allow('create', true)
64+
@@allow('all', members?[user == auth() && type == OWNER])
65+
@@allow('read', members?[user == auth()])
66+
}
67+
68+
69+
model Member extends IBase {
70+
type MemberType @default(MEMBER)
71+
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
72+
organizationId String
73+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
74+
userId String
75+
76+
@@id([organizationId, userId])
77+
@@allow('all', organization.members?[user == auth() && type == OWNER])
78+
@@allow('read', user == auth())
79+
}
80+
81+
/* Google Stuff */
82+
model GoogleIntegration extends IIntegration {
83+
reference String @id
84+
}
85+
`,
86+
{ provider: 'postgresql', dbUrl, logPrismaQuery: true }
87+
);
88+
89+
prisma = r.prisma;
90+
const enhance = r.enhance;
91+
92+
await prisma.organization.create({
93+
data: {
94+
name: 'My Organization',
95+
members: {
96+
create: {
97+
type: 'OWNER',
98+
user: {
99+
create: {
100+
id: '1',
101+
firstName: 'John',
102+
lastName: 'Doe',
103+
},
104+
},
105+
},
106+
},
107+
},
108+
});
109+
110+
const db = enhance({ id: '1' });
111+
await expect(db.organization.findMany()).resolves.toHaveLength(1);
112+
} finally {
113+
if (prisma) {
114+
await prisma.$disconnect();
115+
}
116+
await dropPostgresDb('issue-1435');
117+
}
118+
});
119+
});

0 commit comments

Comments
 (0)