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
15 changes: 9 additions & 6 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ on:
branches: ['dev', 'main', 'canary']

jobs:
build:
build-test:
runs-on: ubuntu-latest

strategy:
Expand All @@ -35,11 +35,14 @@ jobs:
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: |
if [[ $GITHUB_REF == 'refs/heads/canary' ]]; then
DEFAULT_NPM_TAG=canary pnpm run build
else
DEFAULT_NPM_TAG=latest pnpm run build
fi
if [[ $GITHUB_REF == 'refs/heads/canary' ]]; then
DEFAULT_NPM_TAG=canary pnpm run build
else
DEFAULT_NPM_TAG=latest pnpm run build
fi

- run: pnpm lint

# install again for internal dependencies
- run: pnpm install --frozen-lockfile
- run: pnpm run test
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ const MyPosts = () => {
};
```

The following diagram gives a high-level overview of how it works.

![Architecture](https://zenstack.dev/img/architecture-light.png)

## Links

- [Home](https://zenstack.dev)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "zenstack-monorepo",
"version": "1.0.0-alpha.31",
"version": "1.0.0-alpha.33",
"description": "",
"scripts": {
"build": "pnpm -r build",
Expand Down
2 changes: 1 addition & 1 deletion packages/language/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/language",
"version": "1.0.0-alpha.31",
"version": "1.0.0-alpha.33",
"displayName": "ZenStack modeling language compiler",
"description": "ZenStack modeling language compiler",
"homepage": "https://zenstack.dev",
Expand Down
2 changes: 1 addition & 1 deletion packages/next/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/next",
"version": "1.0.0-alpha.31",
"version": "1.0.0-alpha.33",
"displayName": "ZenStack Next.js integration",
"description": "ZenStack Next.js integration",
"homepage": "https://zenstack.dev",
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/react/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/react",
"displayName": "ZenStack plugin and runtime for ReactJS",
"version": "1.0.0-alpha.31",
"version": "1.0.0-alpha.33",
"description": "ZenStack plugin and runtime for ReactJS",
"main": "index.js",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/trpc/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/trpc",
"displayName": "ZenStack plugin for tRPC",
"version": "1.0.0-alpha.31",
"version": "1.0.0-alpha.33",
"description": "ZenStack plugin for tRPC",
"main": "index.js",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/runtime",
"displayName": "ZenStack Runtime Library",
"version": "1.0.0-alpha.31",
"version": "1.0.0-alpha.33",
"description": "Runtime of ZenStack for both client-side and server-side environments.",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion packages/schema/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"publisher": "zenstack",
"displayName": "ZenStack Language Tools",
"description": "A toolkit for building secure CRUD apps with Next.js + Typescript",
"version": "1.0.0-alpha.31",
"version": "1.0.0-alpha.33",
"author": {
"name": "ZenStack Team"
},
Expand Down
4 changes: 4 additions & 0 deletions packages/schema/src/language-server/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ export const SCALAR_TYPES = ['String', 'Int', 'Float', 'Decimal', 'BigInt', 'Boo
* Name of standard library module
*/
export const STD_LIB_MODULE_NAME = 'stdlib.zmodel';

export enum IssueCodes {
MissingOppositeRelation = 'miss-opposite-relation',
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
import { ValidationAcceptor } from 'langium';
import pluralize from 'pluralize';
import { analyzePolicies } from '../../utils/ast-utils';
import { SCALAR_TYPES } from '../constants';
import { IssueCodes, SCALAR_TYPES } from '../constants';
import { AstValidator } from '../types';
import { assignableToAttributeParam, validateDuplicatedDeclarations } from './utils';

Expand Down Expand Up @@ -297,7 +297,7 @@ export default class DataModelValidator implements AstValidator<DataModel> {
accept(
'error',
`The relation field "${field.name}" on model "${field.$container.name}" is missing an opposite relation field on model "${oppositeModel.name}"`,
{ node: field }
{ node: field, code: IssueCodes.MissingOppositeRelation }
);
return;
} else if (oppositeFields.length > 1) {
Expand Down
19 changes: 18 additions & 1 deletion packages/schema/src/language-server/validator/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import {
ExpressionType,
isArrayExpr,
isDataModelField,
isEnum,
isLiteralExpr,
isReferenceExpr,
} from '@zenstackhq/language/ast';
import { resolved } from '@zenstackhq/sdk';
import { AstNode, ValidationAcceptor } from 'langium';

/**
Expand Down Expand Up @@ -99,7 +101,19 @@ export function assignableToAttributeParam(
const dstIsArray = param.type.array;
const dstRef = param.type.reference;

if (dstType) {
if (isEnum(argResolvedType.decl)) {
// enum type

let attrArgDeclType = dstRef?.ref;
if (dstType === 'ContextType' && isDataModelField(attr.$container) && attr.$container?.type?.reference) {
// attribute parameter type is ContextType, need to infer type from
// the attribute's container
attrArgDeclType = resolved(attr.$container?.type?.reference);
}
return attrArgDeclType === argResolvedType.decl && dstIsArray === argResolvedType.array;
} else if (dstType) {
// scalar type

if (typeof argResolvedType?.decl !== 'string') {
// destination type is not a reference, so argument type must be a plain expression
return false;
Expand All @@ -115,6 +129,8 @@ export function assignableToAttributeParam(
return isReferenceExpr(arg.value) && isDataModelField(arg.value.target.ref);
}
} else if (dstType === 'ContextType') {
// attribute parameter type is ContextType, need to infer type from
// the attribute's container
if (isDataModelField(attr.$container)) {
if (!attr.$container?.type?.type) {
return false;
Expand All @@ -129,6 +145,7 @@ export function assignableToAttributeParam(
typeAssignable(dstType, argResolvedType.decl) && (dstType === 'Any' || dstIsArray === argResolvedType.array)
);
} else {
// reference type
return dstRef?.ref === argResolvedType.decl && dstIsArray === argResolvedType.array;
}
}
133 changes: 133 additions & 0 deletions packages/schema/src/language-server/zmodel-code-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { DataModel, DataModelField, isDataModel } from '@zenstackhq/language/ast';
import {
AstReflection,
CodeActionProvider,
findDeclarationNodeAtOffset,
getContainerOfType,
IndexManager,
LangiumDocument,
LangiumServices,
MaybePromise,
} from 'langium';

import { CodeAction, CodeActionKind, CodeActionParams, Command, Diagnostic } from 'vscode-languageserver';
import { IssueCodes } from './constants';
import { ZModelFormatter } from './zmodel-formatter';

export class ZModelCodeActionProvider implements CodeActionProvider {
protected readonly reflection: AstReflection;
protected readonly indexManager: IndexManager;
protected readonly formatter: ZModelFormatter;

constructor(services: LangiumServices) {
this.reflection = services.shared.AstReflection;
this.indexManager = services.shared.workspace.IndexManager;
this.formatter = services.lsp.Formatter as ZModelFormatter;
}

getCodeActions(
document: LangiumDocument,
params: CodeActionParams
): MaybePromise<Array<Command | CodeAction> | undefined> {
const result: CodeAction[] = [];
const acceptor = (ca: CodeAction | undefined) => ca && result.push(ca);
for (const diagnostic of params.context.diagnostics) {
this.createCodeActions(diagnostic, document, acceptor);
}
return result;
}

private createCodeActions(
diagnostic: Diagnostic,
document: LangiumDocument,
accept: (ca: CodeAction | undefined) => void
) {
switch (diagnostic.code) {
case IssueCodes.MissingOppositeRelation:
accept(this.fixMissingOppositeRelation(diagnostic, document));
}

return undefined;
}

private fixMissingOppositeRelation(diagnostic: Diagnostic, document: LangiumDocument): CodeAction | undefined {
const offset = document.textDocument.offsetAt(diagnostic.range.start);
const rootCst = document.parseResult.value.$cstNode;

if (rootCst) {
const cstNode = findDeclarationNodeAtOffset(rootCst, offset);

const astNode = cstNode?.element as DataModelField;

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const oppositeModel = astNode.type.reference!.ref! as DataModel;

const lastField = oppositeModel.fields[oppositeModel.fields.length - 1];

const container = getContainerOfType(cstNode?.element, isDataModel) as DataModel;

const idField = container.fields.find((f) =>
f.attributes.find((attr) => attr.decl.ref?.name === '@id')
) as DataModelField;

if (container && container.$cstNode && idField) {
// indent
let indent = '\t';
const formatOptions = this.formatter.getFormatOptions();
if (formatOptions?.insertSpaces) {
indent = ' '.repeat(formatOptions.tabSize);
}
indent = indent.repeat(this.formatter.getIndent());

const typeName = container.name;
const fieldName = this.lowerCaseFirstLetter(typeName);

// might already exist
let referenceField = '';

const idFieldName = idField.name;
const referenceIdFieldName = fieldName + this.upperCaseFirstLetter(idFieldName);

if (!oppositeModel.fields.find((f) => f.name === referenceIdFieldName)) {
referenceField = '\n' + indent + `${referenceIdFieldName} ${idField.type.type}`;
}

return {
title: `Add opposite relation fields on ${oppositeModel.name}`,
kind: CodeActionKind.QuickFix,
diagnostics: [diagnostic],
isPreferred: false,
edit: {
changes: {
[document.textDocument.uri]: [
{
range: {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
start: lastField.$cstNode!.range.end,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
end: lastField.$cstNode!.range.end,
},
newText:
'\n' +
indent +
`${fieldName} ${typeName} @relation(fields: [${referenceIdFieldName}], references: [${idFieldName}])` +
referenceField,
},
],
},
},
};
}
}

return undefined;
}

private lowerCaseFirstLetter(str: string) {
return str.charAt(0).toLowerCase() + str.slice(1);
}

private upperCaseFirstLetter(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
}
39 changes: 37 additions & 2 deletions packages/schema/src/language-server/zmodel-formatter.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
import { AbstractFormatter, AstNode, Formatting } from 'langium';
import { AbstractFormatter, AstNode, Formatting, LangiumDocument } from 'langium';

import * as ast from '@zenstackhq/language/ast';
import { FormattingOptions, Range, TextEdit } from 'vscode-languageserver';

export class ZModelFormatter extends AbstractFormatter {
private formatOptions?: FormattingOptions;
protected format(node: AstNode): void {
const formatter = this.getNodeFormatter(node);
if (ast.isAbstractDeclaration(node)) {
if (ast.isDataModelField(node)) {
formatter.property('type').prepend(Formatting.oneSpace());
if (node.attributes.length > 0) {
formatter.properties('attributes').prepend(Formatting.oneSpace());
}
} else if (ast.isDataModelFieldAttribute(node)) {
formatter.keyword('(').surround(Formatting.noSpace());
formatter.keyword(')').prepend(Formatting.noSpace());
formatter.keyword(',').append(Formatting.oneSpace());
if (node.args.length > 1) {
formatter.nodes(...node.args.slice(1)).prepend(Formatting.oneSpace());
}
} else if (ast.isAttributeArg(node)) {
formatter.keyword(':').prepend(Formatting.noSpace());
formatter.keyword(':').append(Formatting.oneSpace());
} else if (ast.isAbstractDeclaration(node)) {
const bracesOpen = formatter.keyword('{');
const bracesClose = formatter.keyword('}');
// this line decide the indent count return by this.getIndent()
formatter.interior(bracesOpen, bracesClose).prepend(Formatting.indent());
bracesOpen.prepend(Formatting.oneSpace());
bracesClose.prepend(Formatting.newLine());
Expand All @@ -17,4 +35,21 @@ export class ZModelFormatter extends AbstractFormatter {
nodes.prepend(Formatting.noIndent());
}
}

protected override doDocumentFormat(
document: LangiumDocument<AstNode>,
options: FormattingOptions,
range?: Range | undefined
): TextEdit[] {
this.formatOptions = options;
return super.doDocumentFormat(document, options, range);
}

public getFormatOptions(): FormattingOptions | undefined {
return this.formatOptions;
}

public getIndent() {
return 1;
}
}
2 changes: 2 additions & 0 deletions packages/schema/src/language-server/zmodel-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import { TextDocuments } from 'vscode-languageserver';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { ZModelValidationRegistry, ZModelValidator } from './validator/zmodel-validator';
import { ZModelCodeActionProvider } from './zmodel-code-action';
import { ZModelFormatter } from './zmodel-formatter';
import { ZModelLinker } from './zmodel-linker';
import { ZModelScopeComputation } from './zmodel-scope';
Expand Down Expand Up @@ -56,6 +57,7 @@ export const ZModelModule: Module<ZModelServices, PartialLangiumServices & ZMode
},
lsp: {
Formatter: () => new ZModelFormatter(),
CodeActionProvider: (services) => new ZModelCodeActionProvider(services),
},
};

Expand Down
Loading