Skip to content

Commit 9d2cfc7

Browse files
authored
merge dev to main (#205)
2 parents e415370 + ea6bbec commit 9d2cfc7

File tree

18 files changed

+234
-21
lines changed

18 files changed

+234
-21
lines changed

.github/workflows/build-test.yml

+9-6
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ on:
1414
branches: ['dev', 'main', 'canary']
1515

1616
jobs:
17-
build:
17+
build-test:
1818
runs-on: ubuntu-latest
1919

2020
strategy:
@@ -35,11 +35,14 @@ jobs:
3535
cache: 'pnpm'
3636
- run: pnpm install --frozen-lockfile
3737
- run: |
38-
if [[ $GITHUB_REF == 'refs/heads/canary' ]]; then
39-
DEFAULT_NPM_TAG=canary pnpm run build
40-
else
41-
DEFAULT_NPM_TAG=latest pnpm run build
42-
fi
38+
if [[ $GITHUB_REF == 'refs/heads/canary' ]]; then
39+
DEFAULT_NPM_TAG=canary pnpm run build
40+
else
41+
DEFAULT_NPM_TAG=latest pnpm run build
42+
fi
43+
44+
- run: pnpm lint
45+
4346
# install again for internal dependencies
4447
- run: pnpm install --frozen-lockfile
4548
- run: pnpm run test

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ const MyPosts = () => {
8989
};
9090
```
9191

92+
The following diagram gives a high-level overview of how it works.
93+
94+
![Architecture](https://zenstack.dev/img/architecture-light.png)
95+
9296
## Links
9397

9498
- [Home](https://zenstack.dev)

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "zenstack-monorepo",
3-
"version": "1.0.0-alpha.31",
3+
"version": "1.0.0-alpha.33",
44
"description": "",
55
"scripts": {
66
"build": "pnpm -r build",

packages/language/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenstackhq/language",
3-
"version": "1.0.0-alpha.31",
3+
"version": "1.0.0-alpha.33",
44
"displayName": "ZenStack modeling language compiler",
55
"description": "ZenStack modeling language compiler",
66
"homepage": "https://zenstack.dev",

packages/next/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenstackhq/next",
3-
"version": "1.0.0-alpha.31",
3+
"version": "1.0.0-alpha.33",
44
"displayName": "ZenStack Next.js integration",
55
"description": "ZenStack Next.js integration",
66
"homepage": "https://zenstack.dev",

packages/plugins/react/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/react",
33
"displayName": "ZenStack plugin and runtime for ReactJS",
4-
"version": "1.0.0-alpha.31",
4+
"version": "1.0.0-alpha.33",
55
"description": "ZenStack plugin and runtime for ReactJS",
66
"main": "index.js",
77
"repository": {

packages/plugins/trpc/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/trpc",
33
"displayName": "ZenStack plugin for tRPC",
4-
"version": "1.0.0-alpha.31",
4+
"version": "1.0.0-alpha.33",
55
"description": "ZenStack plugin for tRPC",
66
"main": "index.js",
77
"repository": {

packages/runtime/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/runtime",
33
"displayName": "ZenStack Runtime Library",
4-
"version": "1.0.0-alpha.31",
4+
"version": "1.0.0-alpha.33",
55
"description": "Runtime of ZenStack for both client-side and server-side environments.",
66
"repository": {
77
"type": "git",

packages/schema/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"publisher": "zenstack",
44
"displayName": "ZenStack Language Tools",
55
"description": "A toolkit for building secure CRUD apps with Next.js + Typescript",
6-
"version": "1.0.0-alpha.31",
6+
"version": "1.0.0-alpha.33",
77
"author": {
88
"name": "ZenStack Team"
99
},

packages/schema/src/language-server/constants.ts

+4
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,7 @@ export const SCALAR_TYPES = ['String', 'Int', 'Float', 'Decimal', 'BigInt', 'Boo
1212
* Name of standard library module
1313
*/
1414
export const STD_LIB_MODULE_NAME = 'stdlib.zmodel';
15+
16+
export enum IssueCodes {
17+
MissingOppositeRelation = 'miss-opposite-relation',
18+
}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
import { ValidationAcceptor } from 'langium';
1616
import pluralize from 'pluralize';
1717
import { analyzePolicies } from '../../utils/ast-utils';
18-
import { SCALAR_TYPES } from '../constants';
18+
import { IssueCodes, SCALAR_TYPES } from '../constants';
1919
import { AstValidator } from '../types';
2020
import { assignableToAttributeParam, validateDuplicatedDeclarations } from './utils';
2121

@@ -297,7 +297,7 @@ export default class DataModelValidator implements AstValidator<DataModel> {
297297
accept(
298298
'error',
299299
`The relation field "${field.name}" on model "${field.$container.name}" is missing an opposite relation field on model "${oppositeModel.name}"`,
300-
{ node: field }
300+
{ node: field, code: IssueCodes.MissingOppositeRelation }
301301
);
302302
return;
303303
} else if (oppositeFields.length > 1) {

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

+18-1
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import {
77
ExpressionType,
88
isArrayExpr,
99
isDataModelField,
10+
isEnum,
1011
isLiteralExpr,
1112
isReferenceExpr,
1213
} from '@zenstackhq/language/ast';
14+
import { resolved } from '@zenstackhq/sdk';
1315
import { AstNode, ValidationAcceptor } from 'langium';
1416

1517
/**
@@ -99,7 +101,19 @@ export function assignableToAttributeParam(
99101
const dstIsArray = param.type.array;
100102
const dstRef = param.type.reference;
101103

102-
if (dstType) {
104+
if (isEnum(argResolvedType.decl)) {
105+
// enum type
106+
107+
let attrArgDeclType = dstRef?.ref;
108+
if (dstType === 'ContextType' && isDataModelField(attr.$container) && attr.$container?.type?.reference) {
109+
// attribute parameter type is ContextType, need to infer type from
110+
// the attribute's container
111+
attrArgDeclType = resolved(attr.$container?.type?.reference);
112+
}
113+
return attrArgDeclType === argResolvedType.decl && dstIsArray === argResolvedType.array;
114+
} else if (dstType) {
115+
// scalar type
116+
103117
if (typeof argResolvedType?.decl !== 'string') {
104118
// destination type is not a reference, so argument type must be a plain expression
105119
return false;
@@ -115,6 +129,8 @@ export function assignableToAttributeParam(
115129
return isReferenceExpr(arg.value) && isDataModelField(arg.value.target.ref);
116130
}
117131
} else if (dstType === 'ContextType') {
132+
// attribute parameter type is ContextType, need to infer type from
133+
// the attribute's container
118134
if (isDataModelField(attr.$container)) {
119135
if (!attr.$container?.type?.type) {
120136
return false;
@@ -129,6 +145,7 @@ export function assignableToAttributeParam(
129145
typeAssignable(dstType, argResolvedType.decl) && (dstType === 'Any' || dstIsArray === argResolvedType.array)
130146
);
131147
} else {
148+
// reference type
132149
return dstRef?.ref === argResolvedType.decl && dstIsArray === argResolvedType.array;
133150
}
134151
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { DataModel, DataModelField, isDataModel } from '@zenstackhq/language/ast';
2+
import {
3+
AstReflection,
4+
CodeActionProvider,
5+
findDeclarationNodeAtOffset,
6+
getContainerOfType,
7+
IndexManager,
8+
LangiumDocument,
9+
LangiumServices,
10+
MaybePromise,
11+
} from 'langium';
12+
13+
import { CodeAction, CodeActionKind, CodeActionParams, Command, Diagnostic } from 'vscode-languageserver';
14+
import { IssueCodes } from './constants';
15+
import { ZModelFormatter } from './zmodel-formatter';
16+
17+
export class ZModelCodeActionProvider implements CodeActionProvider {
18+
protected readonly reflection: AstReflection;
19+
protected readonly indexManager: IndexManager;
20+
protected readonly formatter: ZModelFormatter;
21+
22+
constructor(services: LangiumServices) {
23+
this.reflection = services.shared.AstReflection;
24+
this.indexManager = services.shared.workspace.IndexManager;
25+
this.formatter = services.lsp.Formatter as ZModelFormatter;
26+
}
27+
28+
getCodeActions(
29+
document: LangiumDocument,
30+
params: CodeActionParams
31+
): MaybePromise<Array<Command | CodeAction> | undefined> {
32+
const result: CodeAction[] = [];
33+
const acceptor = (ca: CodeAction | undefined) => ca && result.push(ca);
34+
for (const diagnostic of params.context.diagnostics) {
35+
this.createCodeActions(diagnostic, document, acceptor);
36+
}
37+
return result;
38+
}
39+
40+
private createCodeActions(
41+
diagnostic: Diagnostic,
42+
document: LangiumDocument,
43+
accept: (ca: CodeAction | undefined) => void
44+
) {
45+
switch (diagnostic.code) {
46+
case IssueCodes.MissingOppositeRelation:
47+
accept(this.fixMissingOppositeRelation(diagnostic, document));
48+
}
49+
50+
return undefined;
51+
}
52+
53+
private fixMissingOppositeRelation(diagnostic: Diagnostic, document: LangiumDocument): CodeAction | undefined {
54+
const offset = document.textDocument.offsetAt(diagnostic.range.start);
55+
const rootCst = document.parseResult.value.$cstNode;
56+
57+
if (rootCst) {
58+
const cstNode = findDeclarationNodeAtOffset(rootCst, offset);
59+
60+
const astNode = cstNode?.element as DataModelField;
61+
62+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
63+
const oppositeModel = astNode.type.reference!.ref! as DataModel;
64+
65+
const lastField = oppositeModel.fields[oppositeModel.fields.length - 1];
66+
67+
const container = getContainerOfType(cstNode?.element, isDataModel) as DataModel;
68+
69+
const idField = container.fields.find((f) =>
70+
f.attributes.find((attr) => attr.decl.ref?.name === '@id')
71+
) as DataModelField;
72+
73+
if (container && container.$cstNode && idField) {
74+
// indent
75+
let indent = '\t';
76+
const formatOptions = this.formatter.getFormatOptions();
77+
if (formatOptions?.insertSpaces) {
78+
indent = ' '.repeat(formatOptions.tabSize);
79+
}
80+
indent = indent.repeat(this.formatter.getIndent());
81+
82+
const typeName = container.name;
83+
const fieldName = this.lowerCaseFirstLetter(typeName);
84+
85+
// might already exist
86+
let referenceField = '';
87+
88+
const idFieldName = idField.name;
89+
const referenceIdFieldName = fieldName + this.upperCaseFirstLetter(idFieldName);
90+
91+
if (!oppositeModel.fields.find((f) => f.name === referenceIdFieldName)) {
92+
referenceField = '\n' + indent + `${referenceIdFieldName} ${idField.type.type}`;
93+
}
94+
95+
return {
96+
title: `Add opposite relation fields on ${oppositeModel.name}`,
97+
kind: CodeActionKind.QuickFix,
98+
diagnostics: [diagnostic],
99+
isPreferred: false,
100+
edit: {
101+
changes: {
102+
[document.textDocument.uri]: [
103+
{
104+
range: {
105+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
106+
start: lastField.$cstNode!.range.end,
107+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
108+
end: lastField.$cstNode!.range.end,
109+
},
110+
newText:
111+
'\n' +
112+
indent +
113+
`${fieldName} ${typeName} @relation(fields: [${referenceIdFieldName}], references: [${idFieldName}])` +
114+
referenceField,
115+
},
116+
],
117+
},
118+
},
119+
};
120+
}
121+
}
122+
123+
return undefined;
124+
}
125+
126+
private lowerCaseFirstLetter(str: string) {
127+
return str.charAt(0).toLowerCase() + str.slice(1);
128+
}
129+
130+
private upperCaseFirstLetter(str: string) {
131+
return str.charAt(0).toUpperCase() + str.slice(1);
132+
}
133+
}
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,31 @@
1-
import { AbstractFormatter, AstNode, Formatting } from 'langium';
1+
import { AbstractFormatter, AstNode, Formatting, LangiumDocument } from 'langium';
22

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

56
export class ZModelFormatter extends AbstractFormatter {
7+
private formatOptions?: FormattingOptions;
68
protected format(node: AstNode): void {
79
const formatter = this.getNodeFormatter(node);
8-
if (ast.isAbstractDeclaration(node)) {
10+
if (ast.isDataModelField(node)) {
11+
formatter.property('type').prepend(Formatting.oneSpace());
12+
if (node.attributes.length > 0) {
13+
formatter.properties('attributes').prepend(Formatting.oneSpace());
14+
}
15+
} else if (ast.isDataModelFieldAttribute(node)) {
16+
formatter.keyword('(').surround(Formatting.noSpace());
17+
formatter.keyword(')').prepend(Formatting.noSpace());
18+
formatter.keyword(',').append(Formatting.oneSpace());
19+
if (node.args.length > 1) {
20+
formatter.nodes(...node.args.slice(1)).prepend(Formatting.oneSpace());
21+
}
22+
} else if (ast.isAttributeArg(node)) {
23+
formatter.keyword(':').prepend(Formatting.noSpace());
24+
formatter.keyword(':').append(Formatting.oneSpace());
25+
} else if (ast.isAbstractDeclaration(node)) {
926
const bracesOpen = formatter.keyword('{');
1027
const bracesClose = formatter.keyword('}');
28+
// this line decide the indent count return by this.getIndent()
1129
formatter.interior(bracesOpen, bracesClose).prepend(Formatting.indent());
1230
bracesOpen.prepend(Formatting.oneSpace());
1331
bracesClose.prepend(Formatting.newLine());
@@ -17,4 +35,21 @@ export class ZModelFormatter extends AbstractFormatter {
1735
nodes.prepend(Formatting.noIndent());
1836
}
1937
}
38+
39+
protected override doDocumentFormat(
40+
document: LangiumDocument<AstNode>,
41+
options: FormattingOptions,
42+
range?: Range | undefined
43+
): TextEdit[] {
44+
this.formatOptions = options;
45+
return super.doDocumentFormat(document, options, range);
46+
}
47+
48+
public getFormatOptions(): FormattingOptions | undefined {
49+
return this.formatOptions;
50+
}
51+
52+
public getIndent() {
53+
return 1;
54+
}
2055
}

packages/schema/src/language-server/zmodel-module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import { TextDocuments } from 'vscode-languageserver';
2121
import { TextDocument } from 'vscode-languageserver-textdocument';
2222
import { ZModelValidationRegistry, ZModelValidator } from './validator/zmodel-validator';
23+
import { ZModelCodeActionProvider } from './zmodel-code-action';
2324
import { ZModelFormatter } from './zmodel-formatter';
2425
import { ZModelLinker } from './zmodel-linker';
2526
import { ZModelScopeComputation } from './zmodel-scope';
@@ -56,6 +57,7 @@ export const ZModelModule: Module<ZModelServices, PartialLangiumServices & ZMode
5657
},
5758
lsp: {
5859
Formatter: () => new ZModelFormatter(),
60+
CodeActionProvider: (services) => new ZModelCodeActionProvider(services),
5961
},
6062
};
6163

0 commit comments

Comments
 (0)