Skip to content

Commit af151b7

Browse files
authored
feat: support Prisma view (#579)
1 parent fc5e394 commit af151b7

File tree

15 files changed

+1581
-74
lines changed

15 files changed

+1581
-74
lines changed

packages/language/src/generated/ast.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ export interface DataModel extends AstNode {
194194
comments: Array<string>
195195
fields: Array<DataModelField>
196196
isAbstract: boolean
197+
isView: boolean
197198
name: RegularID
198199
superTypes: Array<Reference<DataModel>>
199200
}
@@ -748,6 +749,7 @@ export class ZModelAstReflection extends AbstractAstReflection {
748749
{ name: 'comments', type: 'array' },
749750
{ name: 'fields', type: 'array' },
750751
{ name: 'isAbstract', type: 'boolean' },
752+
{ name: 'isView', type: 'boolean' },
751753
{ name: 'superTypes', type: 'array' }
752754
]
753755
};

packages/language/src/generated/grammar.ts

Lines changed: 88 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1681,74 +1681,110 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
16811681
"cardinality": "*"
16821682
},
16831683
{
1684-
"$type": "Assignment",
1685-
"feature": "isAbstract",
1686-
"operator": "?=",
1687-
"terminal": {
1688-
"$type": "Keyword",
1689-
"value": "abstract"
1690-
},
1691-
"cardinality": "?"
1692-
},
1693-
{
1694-
"$type": "Keyword",
1695-
"value": "model"
1696-
},
1697-
{
1698-
"$type": "Assignment",
1699-
"feature": "name",
1700-
"operator": "=",
1701-
"terminal": {
1702-
"$type": "RuleCall",
1703-
"rule": {
1704-
"$ref": "#/rules@40"
1705-
},
1706-
"arguments": []
1707-
}
1708-
},
1709-
{
1710-
"$type": "Group",
1684+
"$type": "Alternatives",
17111685
"elements": [
17121686
{
1713-
"$type": "Keyword",
1714-
"value": "extends"
1715-
},
1716-
{
1717-
"$type": "Assignment",
1718-
"feature": "superTypes",
1719-
"operator": "+=",
1720-
"terminal": {
1721-
"$type": "CrossReference",
1722-
"type": {
1723-
"$ref": "#/rules@30"
1687+
"$type": "Group",
1688+
"elements": [
1689+
{
1690+
"$type": "Assignment",
1691+
"feature": "isAbstract",
1692+
"operator": "?=",
1693+
"terminal": {
1694+
"$type": "Keyword",
1695+
"value": "abstract"
1696+
},
1697+
"cardinality": "?"
17241698
},
1725-
"deprecatedSyntax": false
1726-
}
1699+
{
1700+
"$type": "Keyword",
1701+
"value": "model"
1702+
},
1703+
{
1704+
"$type": "Assignment",
1705+
"feature": "name",
1706+
"operator": "=",
1707+
"terminal": {
1708+
"$type": "RuleCall",
1709+
"rule": {
1710+
"$ref": "#/rules@40"
1711+
},
1712+
"arguments": []
1713+
}
1714+
},
1715+
{
1716+
"$type": "Group",
1717+
"elements": [
1718+
{
1719+
"$type": "Keyword",
1720+
"value": "extends"
1721+
},
1722+
{
1723+
"$type": "Assignment",
1724+
"feature": "superTypes",
1725+
"operator": "+=",
1726+
"terminal": {
1727+
"$type": "CrossReference",
1728+
"type": {
1729+
"$ref": "#/rules@30"
1730+
},
1731+
"deprecatedSyntax": false
1732+
}
1733+
},
1734+
{
1735+
"$type": "Group",
1736+
"elements": [
1737+
{
1738+
"$type": "Keyword",
1739+
"value": ","
1740+
},
1741+
{
1742+
"$type": "Assignment",
1743+
"feature": "superTypes",
1744+
"operator": "+=",
1745+
"terminal": {
1746+
"$type": "CrossReference",
1747+
"type": {
1748+
"$ref": "#/rules@30"
1749+
},
1750+
"deprecatedSyntax": false
1751+
}
1752+
}
1753+
],
1754+
"cardinality": "*"
1755+
}
1756+
],
1757+
"cardinality": "?"
1758+
}
1759+
]
17271760
},
17281761
{
17291762
"$type": "Group",
17301763
"elements": [
17311764
{
1732-
"$type": "Keyword",
1733-
"value": ","
1765+
"$type": "Assignment",
1766+
"feature": "isView",
1767+
"operator": "?=",
1768+
"terminal": {
1769+
"$type": "Keyword",
1770+
"value": "view"
1771+
}
17341772
},
17351773
{
17361774
"$type": "Assignment",
1737-
"feature": "superTypes",
1738-
"operator": "+=",
1775+
"feature": "name",
1776+
"operator": "=",
17391777
"terminal": {
1740-
"$type": "CrossReference",
1741-
"type": {
1742-
"$ref": "#/rules@30"
1778+
"$type": "RuleCall",
1779+
"rule": {
1780+
"$ref": "#/rules@40"
17431781
},
1744-
"deprecatedSyntax": false
1782+
"arguments": []
17451783
}
17461784
}
1747-
],
1748-
"cardinality": "*"
1785+
]
17491786
}
1750-
],
1751-
"cardinality": "?"
1787+
]
17521788
},
17531789
{
17541790
"$type": "Keyword",

packages/language/src/zmodel.langium

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,12 @@ Argument:
156156
// model
157157
DataModel:
158158
(comments+=TRIPLE_SLASH_COMMENT)*
159-
(isAbstract?='abstract')? 'model' name=RegularID
160-
('extends' superTypes+=[DataModel] (',' superTypes+=[DataModel])*)? '{' (
159+
(
160+
((isAbstract?='abstract')? 'model' name=RegularID
161+
('extends' superTypes+=[DataModel] (',' superTypes+=[DataModel])*)?) |
162+
((isView?='view') name=RegularID)
163+
)
164+
'{' (
161165
fields+=DataModelField
162166
| attributes+=DataModelAttribute
163167
)+

packages/language/syntaxes/zmodel.tmLanguage.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
},
1111
{
1212
"name": "keyword.control.zmodel",
13-
"match": "\\b(Any|Asc|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|Desc|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|Unsupported|abstract|attribute|datasource|enum|extends|false|function|generator|import|in|model|plugin|sort|true)\\b"
13+
"match": "\\b(Any|Asc|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|Desc|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|Unsupported|abstract|attribute|datasource|enum|extends|false|function|generator|import|in|model|plugin|sort|true|view)\\b"
1414
},
1515
{
1616
"name": "string.quoted.double.zmodel",

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

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
isLiteralExpr,
77
ReferenceExpr,
88
} from '@zenstackhq/language/ast';
9-
import { analyzePolicies, getIdFields, getLiteral } from '@zenstackhq/sdk';
9+
import { analyzePolicies, getModelIdFields, getModelUniqueFields, getLiteral } from '@zenstackhq/sdk';
1010
import { AstNode, DiagnosticInfo, getDocument, ValidationAcceptor } from 'langium';
1111
import { IssueCodes, SCALAR_TYPES } from '../constants';
1212
import { AstValidator } from '../types';
@@ -26,16 +26,29 @@ export default class DataModelValidator implements AstValidator<DataModel> {
2626

2727
private validateFields(dm: DataModel, accept: ValidationAcceptor) {
2828
const idFields = dm.$resolvedFields.filter((f) => f.attributes.find((attr) => attr.decl.ref?.name === '@id'));
29-
const modelLevelIds = getIdFields(dm);
30-
31-
if (idFields.length === 0 && modelLevelIds.length === 0) {
29+
const uniqueFields = dm.$resolvedFields.filter((f) =>
30+
f.attributes.find((attr) => attr.decl.ref?.name === '@unique')
31+
);
32+
const modelLevelIds = getModelIdFields(dm);
33+
const modelUniqueFields = getModelUniqueFields(dm);
34+
35+
if (
36+
idFields.length === 0 &&
37+
modelLevelIds.length === 0 &&
38+
uniqueFields.length === 0 &&
39+
modelUniqueFields.length === 0
40+
) {
3241
const { allows, denies, hasFieldValidation } = analyzePolicies(dm);
3342
if (allows.length > 0 || denies.length > 0 || hasFieldValidation) {
3443
// TODO: relax this requirement to require only @unique fields
3544
// when access policies or field valdaition is used, require an @id field
36-
accept('error', 'Model must include a field with @id attribute or a model-level @@id attribute', {
37-
node: dm,
38-
});
45+
accept(
46+
'error',
47+
'Model must include a field with @id or @unique attribute, or a model-level @@id or @@unique attribute to use access policies',
48+
{
49+
node: dm,
50+
}
51+
);
3952
}
4053
} else if (idFields.length > 0 && modelLevelIds.length > 0) {
4154
accept('error', 'Model cannot have both field-level @id and model-level @@id attributes', {

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@ export class PrismaModel {
3535
}
3636

3737
addModel(name: string): Model {
38-
const model = new Model(name);
38+
const model = new Model(name, false);
39+
this.models.push(model);
40+
return model;
41+
}
42+
43+
addView(name: string): Model {
44+
const model = new Model(name, true);
3945
this.models.push(model);
4046
return model;
4147
}
@@ -127,7 +133,7 @@ export class FieldDeclaration extends DeclarationBase {
127133

128134
export class Model extends ContainerDeclaration {
129135
public fields: ModelField[] = [];
130-
constructor(public name: string, documentations: string[] = []) {
136+
constructor(public name: string, public isView: boolean, documentations: string[] = []) {
131137
super(documentations);
132138
}
133139

@@ -164,7 +170,7 @@ export class Model extends ContainerDeclaration {
164170
result.push(...this.attributes);
165171
return (
166172
super.toString() +
167-
`model ${this.name} {\n` +
173+
`${this.isView ? 'view' : 'model'} ${this.name} {\n` +
168174
indentString(result.map((d) => d.toString()).join('\n')) +
169175
`\n}`
170176
);

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ export default class PrismaSchemaGenerator {
277277
}
278278

279279
private generateModel(prisma: PrismaModel, decl: DataModel, config?: Record<string, string>) {
280-
const model = prisma.addModel(decl.name);
280+
const model = decl.isView ? prisma.addView(decl.name) : prisma.addModel(decl.name);
281281
for (const field of decl.fields) {
282282
this.generateModelField(model, field);
283283
}
@@ -336,6 +336,10 @@ export default class PrismaSchemaGenerator {
336336
}
337337

338338
private shouldGenerateAuxFields(decl: DataModel) {
339+
if (decl.isView) {
340+
return false;
341+
}
342+
339343
const { allowAll, denyAll, hasFieldValidation } = analyzePolicies(decl);
340344

341345
if (!allowAll && !denyAll) {

packages/schema/tests/generator/prisma-generator.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,4 +523,38 @@ describe('Prisma generator test', () => {
523523
expect(post?.fields.map((f) => f.name)).toContain('zenstack_guard');
524524
expect(post?.fields.map((f) => f.name)).toContain('zenstack_transaction');
525525
});
526+
527+
it('view support', async () => {
528+
const model = await loadModel(`
529+
datasource db {
530+
provider = 'postgresql'
531+
url = env('URL')
532+
}
533+
534+
generator client {
535+
provider = "prisma-client-js"
536+
previewFeatures = ["views"]
537+
}
538+
539+
view UserInfo {
540+
id Int @unique
541+
email String
542+
name String
543+
bio String
544+
}
545+
`);
546+
547+
const { name } = tmp.fileSync({ postfix: '.prisma' });
548+
await new PrismaSchemaGenerator().generate(model, {
549+
name: 'Prisma',
550+
provider: '@core/prisma',
551+
schemaPath: 'schema.zmodel',
552+
output: name,
553+
format: false,
554+
generateClient: false,
555+
});
556+
557+
const content = fs.readFileSync(name, 'utf-8');
558+
await getDMMF({ datamodel: content });
559+
});
526560
});

packages/schema/tests/schema/parser.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
/* eslint-disable @typescript-eslint/ban-types */
2+
/// <reference types="@types/jest" />
3+
24
import {
35
ArrayExpr,
46
AttributeArg,
@@ -509,4 +511,17 @@ describe('Parsing Tests', () => {
509511
`;
510512
await loadModel(content, false);
511513
});
514+
515+
it('view support', async () => {
516+
const content = `
517+
view UserInfo {
518+
id Int @unique
519+
email String
520+
name String
521+
bio String
522+
}
523+
`;
524+
const doc = await loadModel(content, false);
525+
expect((doc.declarations[0] as DataModel).isView).toBeTruthy();
526+
});
512527
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import * as fs from 'fs';
2+
import path from 'path';
3+
import { loadModel } from '../utils';
4+
5+
describe('Trigger.dev Schema Tests', () => {
6+
it('model loading', async () => {
7+
const content = fs.readFileSync(path.join(__dirname, './trigger-dev.zmodel'), {
8+
encoding: 'utf-8',
9+
});
10+
await loadModel(content);
11+
});
12+
});

0 commit comments

Comments
 (0)