Skip to content

feat: support Prisma view #579

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

Merged
merged 4 commits into from
Jul 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/language/src/generated/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export interface DataModel extends AstNode {
comments: Array<string>
fields: Array<DataModelField>
isAbstract: boolean
isView: boolean
name: RegularID
superTypes: Array<Reference<DataModel>>
}
Expand Down Expand Up @@ -748,6 +749,7 @@ export class ZModelAstReflection extends AbstractAstReflection {
{ name: 'comments', type: 'array' },
{ name: 'fields', type: 'array' },
{ name: 'isAbstract', type: 'boolean' },
{ name: 'isView', type: 'boolean' },
{ name: 'superTypes', type: 'array' }
]
};
Expand Down
140 changes: 88 additions & 52 deletions packages/language/src/generated/grammar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1681,74 +1681,110 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"cardinality": "*"
},
{
"$type": "Assignment",
"feature": "isAbstract",
"operator": "?=",
"terminal": {
"$type": "Keyword",
"value": "abstract"
},
"cardinality": "?"
},
{
"$type": "Keyword",
"value": "model"
},
{
"$type": "Assignment",
"feature": "name",
"operator": "=",
"terminal": {
"$type": "RuleCall",
"rule": {
"$ref": "#/rules@40"
},
"arguments": []
}
},
{
"$type": "Group",
"$type": "Alternatives",
"elements": [
{
"$type": "Keyword",
"value": "extends"
},
{
"$type": "Assignment",
"feature": "superTypes",
"operator": "+=",
"terminal": {
"$type": "CrossReference",
"type": {
"$ref": "#/rules@30"
"$type": "Group",
"elements": [
{
"$type": "Assignment",
"feature": "isAbstract",
"operator": "?=",
"terminal": {
"$type": "Keyword",
"value": "abstract"
},
"cardinality": "?"
},
"deprecatedSyntax": false
}
{
"$type": "Keyword",
"value": "model"
},
{
"$type": "Assignment",
"feature": "name",
"operator": "=",
"terminal": {
"$type": "RuleCall",
"rule": {
"$ref": "#/rules@40"
},
"arguments": []
}
},
{
"$type": "Group",
"elements": [
{
"$type": "Keyword",
"value": "extends"
},
{
"$type": "Assignment",
"feature": "superTypes",
"operator": "+=",
"terminal": {
"$type": "CrossReference",
"type": {
"$ref": "#/rules@30"
},
"deprecatedSyntax": false
}
},
{
"$type": "Group",
"elements": [
{
"$type": "Keyword",
"value": ","
},
{
"$type": "Assignment",
"feature": "superTypes",
"operator": "+=",
"terminal": {
"$type": "CrossReference",
"type": {
"$ref": "#/rules@30"
},
"deprecatedSyntax": false
}
}
],
"cardinality": "*"
}
],
"cardinality": "?"
}
]
},
{
"$type": "Group",
"elements": [
{
"$type": "Keyword",
"value": ","
"$type": "Assignment",
"feature": "isView",
"operator": "?=",
"terminal": {
"$type": "Keyword",
"value": "view"
}
},
{
"$type": "Assignment",
"feature": "superTypes",
"operator": "+=",
"feature": "name",
"operator": "=",
"terminal": {
"$type": "CrossReference",
"type": {
"$ref": "#/rules@30"
"$type": "RuleCall",
"rule": {
"$ref": "#/rules@40"
},
"deprecatedSyntax": false
"arguments": []
}
}
],
"cardinality": "*"
]
}
],
"cardinality": "?"
]
},
{
"$type": "Keyword",
Expand Down
8 changes: 6 additions & 2 deletions packages/language/src/zmodel.langium
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,12 @@ Argument:
// model
DataModel:
(comments+=TRIPLE_SLASH_COMMENT)*
(isAbstract?='abstract')? 'model' name=RegularID
('extends' superTypes+=[DataModel] (',' superTypes+=[DataModel])*)? '{' (
(
((isAbstract?='abstract')? 'model' name=RegularID
('extends' superTypes+=[DataModel] (',' superTypes+=[DataModel])*)?) |
((isView?='view') name=RegularID)
)
'{' (
fields+=DataModelField
| attributes+=DataModelAttribute
)+
Expand Down
2 changes: 1 addition & 1 deletion packages/language/syntaxes/zmodel.tmLanguage.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
{
"name": "keyword.control.zmodel",
"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"
"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"
},
{
"name": "string.quoted.double.zmodel",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
isLiteralExpr,
ReferenceExpr,
} from '@zenstackhq/language/ast';
import { analyzePolicies, getIdFields, getLiteral } from '@zenstackhq/sdk';
import { analyzePolicies, getModelIdFields, getModelUniqueFields, getLiteral } from '@zenstackhq/sdk';
import { AstNode, DiagnosticInfo, getDocument, ValidationAcceptor } from 'langium';
import { IssueCodes, SCALAR_TYPES } from '../constants';
import { AstValidator } from '../types';
Expand All @@ -26,16 +26,29 @@ export default class DataModelValidator implements AstValidator<DataModel> {

private validateFields(dm: DataModel, accept: ValidationAcceptor) {
const idFields = dm.$resolvedFields.filter((f) => f.attributes.find((attr) => attr.decl.ref?.name === '@id'));
const modelLevelIds = getIdFields(dm);

if (idFields.length === 0 && modelLevelIds.length === 0) {
const uniqueFields = dm.$resolvedFields.filter((f) =>
f.attributes.find((attr) => attr.decl.ref?.name === '@unique')
);
const modelLevelIds = getModelIdFields(dm);
const modelUniqueFields = getModelUniqueFields(dm);

if (
idFields.length === 0 &&
modelLevelIds.length === 0 &&
uniqueFields.length === 0 &&
modelUniqueFields.length === 0
) {
const { allows, denies, hasFieldValidation } = analyzePolicies(dm);
if (allows.length > 0 || denies.length > 0 || hasFieldValidation) {
// TODO: relax this requirement to require only @unique fields
// when access policies or field valdaition is used, require an @id field
accept('error', 'Model must include a field with @id attribute or a model-level @@id attribute', {
node: dm,
});
accept(
'error',
'Model must include a field with @id or @unique attribute, or a model-level @@id or @@unique attribute to use access policies',
{
node: dm,
}
);
}
} else if (idFields.length > 0 && modelLevelIds.length > 0) {
accept('error', 'Model cannot have both field-level @id and model-level @@id attributes', {
Expand Down
12 changes: 9 additions & 3 deletions packages/schema/src/plugins/prisma/prisma-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ export class PrismaModel {
}

addModel(name: string): Model {
const model = new Model(name);
const model = new Model(name, false);
this.models.push(model);
return model;
}

addView(name: string): Model {
const model = new Model(name, true);
this.models.push(model);
return model;
}
Expand Down Expand Up @@ -127,7 +133,7 @@ export class FieldDeclaration extends DeclarationBase {

export class Model extends ContainerDeclaration {
public fields: ModelField[] = [];
constructor(public name: string, documentations: string[] = []) {
constructor(public name: string, public isView: boolean, documentations: string[] = []) {
super(documentations);
}

Expand Down Expand Up @@ -164,7 +170,7 @@ export class Model extends ContainerDeclaration {
result.push(...this.attributes);
return (
super.toString() +
`model ${this.name} {\n` +
`${this.isView ? 'view' : 'model'} ${this.name} {\n` +
indentString(result.map((d) => d.toString()).join('\n')) +
`\n}`
);
Expand Down
6 changes: 5 additions & 1 deletion packages/schema/src/plugins/prisma/schema-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ export default class PrismaSchemaGenerator {
}

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

private shouldGenerateAuxFields(decl: DataModel) {
if (decl.isView) {
return false;
}

const { allowAll, denyAll, hasFieldValidation } = analyzePolicies(decl);

if (!allowAll && !denyAll) {
Expand Down
34 changes: 34 additions & 0 deletions packages/schema/tests/generator/prisma-generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,4 +523,38 @@ describe('Prisma generator test', () => {
expect(post?.fields.map((f) => f.name)).toContain('zenstack_guard');
expect(post?.fields.map((f) => f.name)).toContain('zenstack_transaction');
});

it('view support', async () => {
const model = await loadModel(`
datasource db {
provider = 'postgresql'
url = env('URL')
}

generator client {
provider = "prisma-client-js"
previewFeatures = ["views"]
}

view UserInfo {
id Int @unique
email String
name String
bio String
}
`);

const { name } = tmp.fileSync({ postfix: '.prisma' });
await new PrismaSchemaGenerator().generate(model, {
name: 'Prisma',
provider: '@core/prisma',
schemaPath: 'schema.zmodel',
output: name,
format: false,
generateClient: false,
});

const content = fs.readFileSync(name, 'utf-8');
await getDMMF({ datamodel: content });
});
});
15 changes: 15 additions & 0 deletions packages/schema/tests/schema/parser.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/* eslint-disable @typescript-eslint/ban-types */
/// <reference types="@types/jest" />

import {
ArrayExpr,
AttributeArg,
Expand Down Expand Up @@ -509,4 +511,17 @@ describe('Parsing Tests', () => {
`;
await loadModel(content, false);
});

it('view support', async () => {
const content = `
view UserInfo {
id Int @unique
email String
name String
bio String
}
`;
const doc = await loadModel(content, false);
expect((doc.declarations[0] as DataModel).isView).toBeTruthy();
});
});
12 changes: 12 additions & 0 deletions packages/schema/tests/schema/trigger-dev.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as fs from 'fs';
import path from 'path';
import { loadModel } from '../utils';

describe('Trigger.dev Schema Tests', () => {
it('model loading', async () => {
const content = fs.readFileSync(path.join(__dirname, './trigger-dev.zmodel'), {
encoding: 'utf-8',
});
await loadModel(content);
});
});
Loading