Skip to content
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
4 changes: 3 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@
"pack": "pnpm pack"
},
"dependencies": {
"@types/node": "^20.12.7",
"@types/node": "^18.0.0",
"@zenstackhq/language": "workspace:*",
"@zenstackhq/runtime": "workspace:*",
"@zenstackhq/sdk": "workspace:*",
"async-exit-hook": "^2.0.1",
"colors": "1.4.0",
"commander": "^8.3.0",
Expand All @@ -43,6 +44,7 @@
"typescript": "^5.0.0"
},
"devDependencies": {
"@zenstackhq/testtools": "workspace:*",
"@types/async-exit-hook": "^2.0.0",
"@types/better-sqlite3": "^7.6.13",
"@types/semver": "^7.3.13",
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/actions/generate.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { isPlugin, LiteralExpr, type Model } from '@zenstackhq/language/ast';
import type { CliGenerator } from '@zenstackhq/runtime/client';
import { TsSchemaGenerator } from '@zenstackhq/sdk';
import colors from 'colors';
import fs from 'node:fs';
import path from 'node:path';
import invariant from 'tiny-invariant';
import { PrismaSchemaGenerator } from '../prisma/prisma-schema-generator';
import { TsSchemaGenerator } from '../zmodel/ts-schema-generator';
import { getSchemaFile, loadSchemaDocument } from './action-utils';

type Options = {
Expand All @@ -25,7 +25,7 @@ export async function run(options: Options) {

// generate TS schema
const tsSchemaFile = path.join(outputPath, 'schema.ts');
await new TsSchemaGenerator().generate(schemaFile, tsSchemaFile);
await new TsSchemaGenerator().generate(schemaFile, [], tsSchemaFile);

await runPlugins(model, outputPath, tsSchemaFile);

Expand Down
16 changes: 7 additions & 9 deletions packages/cli/src/prisma/prisma-schema-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,7 @@ import {
import { AstUtils } from 'langium';
import { match, P } from 'ts-pattern';

import {
hasAttribute,
isDelegateModel,
isIdField,
} from '../zmodel/model-utils';
import { ZModelCodeGenerator } from '../zmodel/zmodel-code-generator';
import { ModelUtils, ZModelCodeGenerator } from '@zenstackhq/sdk';
import {
AttributeArgValue,
ModelField,
Expand Down Expand Up @@ -165,7 +160,7 @@ export class PrismaSchemaGenerator {
? prisma.addView(decl.name)
: prisma.addModel(decl.name);
for (const field of decl.fields) {
if (hasAttribute(field, '@computed')) {
if (ModelUtils.hasAttribute(field, '@computed')) {
continue; // skip computed fields
}
// TODO: exclude fields inherited from delegate
Expand Down Expand Up @@ -274,7 +269,7 @@ export class PrismaSchemaGenerator {
(attr) =>
// when building physical schema, exclude `@default` for id fields inherited from delegate base
!(
isIdField(field) &&
ModelUtils.isIdField(field) &&
this.isInheritedFromDelegate(field) &&
attr.decl.$refText === '@default'
)
Expand Down Expand Up @@ -360,7 +355,10 @@ export class PrismaSchemaGenerator {
}

private isInheritedFromDelegate(field: DataModelField) {
return field.$inheritedFrom && isDelegateModel(field.$inheritedFrom);
return (
field.$inheritedFrom &&
ModelUtils.isDelegateModel(field.$inheritedFrom)
);
}

private makeFieldAttribute(attr: DataModelFieldAttribute) {
Expand Down
13 changes: 5 additions & 8 deletions packages/cli/test/ts-schema-gen.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { generateTsSchema } from './utils';
import { generateTsSchema } from '@zenstackhq/testtools';

describe('TypeScript schema generation tests', () => {
it('generates correct data models', async () => {
Expand Down Expand Up @@ -144,8 +144,7 @@ model Post {
kind: 'array',
items: [
{
kind: 'ref',
model: 'Post',
kind: 'field',
field: 'authorId',
},
],
Expand All @@ -157,8 +156,7 @@ model Post {
kind: 'array',
items: [
{
kind: 'ref',
model: 'User',
kind: 'field',
field: 'id',
},
],
Expand All @@ -167,9 +165,8 @@ model Post {
{
name: 'onDelete',
value: {
kind: 'ref',
model: 'ReferentialAction',
field: 'Cascade',
kind: 'literal',
value: 'Cascade',
},
},
],
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ export default defineConfig({
sourcemap: true,
clean: true,
dts: true,
format: ['esm'],
format: ['esm', 'cjs'],
});
12 changes: 12 additions & 0 deletions packages/language/res/stdlib.zmodel
Original file line number Diff line number Diff line change
Expand Up @@ -707,3 +707,15 @@ attribute @json() @@@targetField([TypeDefField])
* Marks a field to be computed.
*/
attribute @computed()

/**
* Gets the current login user.
*/
function auth(): Any {
} @@@expressionContext([DefaultValue, AccessPolicy])

/**
* Used to specify the model for resolving `auth()` function call in access policies. A Zmodel
* can have at most one model with this attribute. By default, the model named "User" is used.
*/
attribute @@auth() @@@supportTypeDef
23 changes: 18 additions & 5 deletions packages/language/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ export class DocumentLoadError extends Error {
}

export async function loadDocument(
fileName: string
fileName: string,
pluginModelFiles: string[] = []
): Promise<
| { success: true; model: Model; warnings: string[] }
| { success: false; errors: string[]; warnings: string[] }
Expand Down Expand Up @@ -55,16 +56,28 @@ export async function loadDocument(
)
);

const langiumDocuments = services.shared.workspace.LangiumDocuments;
// load plugin model files
const pluginDocs = await Promise.all(
pluginModelFiles.map((file) =>
services.shared.workspace.LangiumDocuments.getOrCreateDocument(
URI.file(path.resolve(file))
)
)
);

// load the document
const langiumDocuments = services.shared.workspace.LangiumDocuments;
const document = await langiumDocuments.getOrCreateDocument(
URI.file(path.resolve(fileName))
);

// build the document together with standard library, plugin modules, and imported documents
await services.shared.workspace.DocumentBuilder.build([stdLib, document], {
validation: true,
});
await services.shared.workspace.DocumentBuilder.build(
[stdLib, ...pluginDocs, document],
{
validation: true,
}
);

const diagnostics = langiumDocuments.all
.flatMap((doc) =>
Expand Down
40 changes: 20 additions & 20 deletions packages/language/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,13 @@ export function isFromStdlib(node: AstNode) {
);
}

// export function isAuthInvocation(node: AstNode) {
// return (
// isInvocationExpr(node) &&
// node.function.ref?.name === 'auth' &&
// isFromStdlib(node.function.ref)
// );
// }
export function isAuthInvocation(node: AstNode) {
return (
isInvocationExpr(node) &&
node.function.ref?.name === 'auth' &&
isFromStdlib(node.function.ref)
);
}

/**
* Try getting string value from a potential string literal expression
Expand Down Expand Up @@ -161,12 +161,12 @@ export function mapBuiltinTypeToExpressionType(
}
}

// export function isAuthOrAuthMemberAccess(expr: Expression): boolean {
// return (
// isAuthInvocation(expr) ||
// (isMemberAccessExpr(expr) && isAuthOrAuthMemberAccess(expr.operand))
// );
// }
export function isAuthOrAuthMemberAccess(expr: Expression): boolean {
return (
isAuthInvocation(expr) ||
(isMemberAccessExpr(expr) && isAuthOrAuthMemberAccess(expr.operand))
);
}

export function isEnumFieldReference(node: AstNode): node is ReferenceExpr {
return isReferenceExpr(node) && isEnumField(node.target.ref);
Expand Down Expand Up @@ -598,13 +598,13 @@ export function getAllDeclarationsIncludingImports(
return model.declarations.concat(...imports.map((imp) => imp.declarations));
}

// export function getAuthDecl(decls: (DataModel | TypeDef)[]) {
// let authModel = decls.find((m) => hasAttribute(m, '@@auth'));
// if (!authModel) {
// authModel = decls.find((m) => m.name === 'User');
// }
// return authModel;
// }
export function getAuthDecl(decls: (DataModel | TypeDef)[]) {
let authModel = decls.find((m) => hasAttribute(m, '@@auth'));
if (!authModel) {
authModel = decls.find((m) => m.name === 'User');
}
return authModel;
}

export function isFutureInvocation(node: AstNode) {
return (
Expand Down
43 changes: 21 additions & 22 deletions packages/language/src/validators/expression-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
import {
findUpAst,
getAttributeArgLiteral,
isAuthInvocation,
isAuthOrAuthMemberAccess,
isDataModelFieldReference,
isEnumFieldReference,
typeAssignable,
Expand All @@ -32,34 +34,32 @@ export default class ExpressionValidator implements AstValidator<Expression> {
validate(expr: Expression, accept: ValidationAcceptor): void {
// deal with a few cases where reference resolution fail silently
if (!expr.$resolvedType) {
// TODO: revisit this
// if (isAuthInvocation(expr)) {
// // check was done at link time
// accept(
// 'error',
// 'auth() cannot be resolved because no model marked with "@@auth()" or named "User" is found',
// { node: expr }
// );
// } else {

const hasReferenceResolutionError = AstUtils.streamAst(expr).some(
(node) => {
if (isAuthInvocation(expr)) {
// check was done at link time
accept(
'error',
'auth() cannot be resolved because no model marked with "@@auth()" or named "User" is found',
{ node: expr }
);
} else {
const hasReferenceResolutionError = AstUtils.streamAst(
expr
).some((node) => {
if (isMemberAccessExpr(node)) {
return !!node.member.error;
}
if (isReferenceExpr(node)) {
return !!node.target.error;
}
return false;
}
);
if (!hasReferenceResolutionError) {
// report silent errors not involving linker errors
accept('error', 'Expression cannot be resolved', {
node: expr,
});
if (!hasReferenceResolutionError) {
// report silent errors not involving linker errors
accept('error', 'Expression cannot be resolved', {
node: expr,
});
}
}
// }
}

// extra validations by expression type
Expand Down Expand Up @@ -379,9 +379,8 @@ export default class ExpressionValidator implements AstValidator<Expression> {
isEnumFieldReference(expr) ||
// null
isNullExpr(expr) ||
// TODO: revise cross-model field comparison
// // `auth()` access
// isAuthOrAuthMemberAccess(expr) ||
// `auth()` access
isAuthOrAuthMemberAccess(expr) ||
// array
(isArrayExpr(expr) &&
expr.items.every((item) => this.isNotModelFieldExpr(item)))
Expand Down
41 changes: 19 additions & 22 deletions packages/language/src/zmodel-linker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ import {
} from './ast';
import {
getAllLoadedAndReachableDataModelsAndTypeDefs,
getAuthDecl,
getContainingDataModel,
getModelFieldsWithBases,
isAuthInvocation,
isFutureExpr,
isMemberContainer,
mapBuiltinTypeToExpressionType,
Expand Down Expand Up @@ -360,23 +362,20 @@ export class ZModelLinker extends DefaultLinker {
// eslint-disable-next-line @typescript-eslint/ban-types
const funcDecl = node.function.ref as FunctionDecl;

// TODO: revisit this
// if (isAuthInvocation(node)) {
// // auth() function is resolved against all loaded and reachable documents
if (isAuthInvocation(node)) {
// auth() function is resolved against all loaded and reachable documents

// // get all data models from loaded and reachable documents
// const allDecls = getAllLoadedAndReachableDataModelsAndTypeDefs(
// this.langiumDocuments(),
// AstUtils.getContainerOfType(node, isDataModel)
// );

// const authDecl = getAuthDecl(allDecls);
// if (authDecl) {
// node.$resolvedType = { decl: authDecl, nullable: true };
// }
// } else
// get all data models from loaded and reachable documents
const allDecls = getAllLoadedAndReachableDataModelsAndTypeDefs(
this.langiumDocuments(),
AstUtils.getContainerOfType(node, isDataModel)
);

if (isFutureExpr(node)) {
const authDecl = getAuthDecl(allDecls);
if (authDecl) {
node.$resolvedType = { decl: authDecl, nullable: true };
}
} else if (isFutureExpr(node)) {
// future() function is resolved to current model
node.$resolvedType = { decl: getContainingDataModel(node) };
} else {
Expand Down Expand Up @@ -413,13 +412,11 @@ export class ZModelLinker extends DefaultLinker {
// member access is resolved only in the context of the operand type
if (node.member.ref) {
this.resolveToDeclaredType(node, node.member.ref.type);

// TODO: revisit this
// if (node.$resolvedType && isAuthInvocation(node.operand)) {
// // member access on auth() function is nullable
// // because user may not have provided all fields
// node.$resolvedType.nullable = true;
// }
if (node.$resolvedType && isAuthInvocation(node.operand)) {
// member access on auth() function is nullable
// because user may not have provided all fields
node.$resolvedType.nullable = true;
}
}
}
}
Expand Down
Loading