Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "zenstack-v3",
"version": "3.0.0-beta.8",
"version": "3.0.0-beta.9",
"description": "ZenStack",
"packageManager": "[email protected]",
"scripts": {
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"publisher": "zenstack",
"displayName": "ZenStack CLI",
"description": "FullStack database toolkit with built-in access control and automatic API generation.",
"version": "3.0.0-beta.8",
"version": "3.0.0-beta.9",
"type": "module",
"author": {
"name": "ZenStack Team"
Expand Down Expand Up @@ -53,7 +53,7 @@
"@zenstackhq/testtools": "workspace:*",
"@zenstackhq/typescript-config": "workspace:*",
"@zenstackhq/vitest-config": "workspace:*",
"better-sqlite3": "^12.2.0",
"better-sqlite3": "catalog:",
"tmp": "catalog:"
}
}
4 changes: 2 additions & 2 deletions packages/cli/src/plugins/prisma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import path from 'node:path';
const plugin: CliPlugin = {
name: 'Prisma Schema Generator',
statusText: 'Generating Prisma schema',
async generate({ model, schemaFile, defaultOutputPath, pluginOptions }) {
async generate({ model, defaultOutputPath, pluginOptions }) {
let outFile = path.join(defaultOutputPath, 'schema.prisma');
if (typeof pluginOptions['output'] === 'string') {
outFile = path.resolve(path.dirname(schemaFile), pluginOptions['output']);
outFile = path.resolve(defaultOutputPath, pluginOptions['output']);
if (!fs.existsSync(path.dirname(outFile))) {
fs.mkdirSync(path.dirname(outFile), { recursive: true });
}
Expand Down
21 changes: 21 additions & 0 deletions packages/cli/test/plugins/prisma-plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,25 @@ model User {
runCli('generate', workDir);
expect(fs.existsSync(path.join(workDir, 'prisma/schema.prisma'))).toBe(true);
});

it('can generate a Prisma schema with custom output relative to zenstack.output', () => {
const workDir = createProject(`
plugin prisma {
provider = '@core/prisma'
output = './schema.prisma'
}

model User {
id String @id @default(cuid())
}
`);

const pkgJson = JSON.parse(fs.readFileSync(path.join(workDir, 'package.json'), 'utf8'));
pkgJson.zenstack = {
output: './relative',
};
fs.writeFileSync(path.join(workDir, 'package.json'), JSON.stringify(pkgJson, null, 2));
runCli('generate', workDir);
expect(fs.existsSync(path.join(workDir, 'relative/schema.prisma'))).toBe(true);
});
});
60 changes: 60 additions & 0 deletions packages/cli/test/ts-schema-gen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,4 +360,64 @@ model User extends Base {
expect(schema.enums).toMatchObject({ Role: expect.any(Object) });
expect(schema.models).toMatchObject({ User: expect.any(Object) });
});

it('generates correct default literal function arguments', async () => {
const { schema } = await generateTsSchema(`
model User {
id String @id @default(uuid(7))
}
`);

expect(schema.models).toMatchObject({
User: {
name: 'User',
fields: {
id: {
name: 'id',
type: 'String',
id: true,
attributes: [
{
name: '@id',
},
{
name: '@default',
args: [
{
name: 'value',
value: {
kind: 'call',
function: 'uuid',
args: [
{
kind: 'literal',
value: 7,
},
],
},
},
],
},
],
default: {
kind: 'call',
function: 'uuid',
args: [
{
kind: 'literal',
value: 7,
},
],
},
},
},
idFields: ['id'],
uniqueFields: {
id: {
type: 'String',
},
},
},
});
});
});
2 changes: 1 addition & 1 deletion packages/common-helpers/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/common-helpers",
"version": "3.0.0-beta.8",
"version": "3.0.0-beta.9",
"description": "ZenStack Common Helpers",
"type": "module",
"scripts": {
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/eslint-config",
"version": "3.0.0-beta.8",
"version": "3.0.0-beta.9",
"type": "module",
"private": true,
"license": "MIT"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/typescript-config",
"version": "3.0.0-beta.8",
"version": "3.0.0-beta.9",
"private": true,
"license": "MIT"
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/vitest-config",
"type": "module",
"version": "3.0.0-beta.8",
"version": "3.0.0-beta.9",
"private": true,
"license": "MIT",
"exports": {
Expand Down
2 changes: 1 addition & 1 deletion packages/create-zenstack/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "create-zenstack",
"version": "3.0.0-beta.8",
"version": "3.0.0-beta.9",
"description": "Create a new ZenStack project",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/dialects/sql.js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/kysely-sql-js",
"version": "3.0.0-beta.8",
"version": "3.0.0-beta.9",
"description": "Kysely dialect for sql.js",
"type": "module",
"scripts": {
Expand Down
3 changes: 2 additions & 1 deletion packages/language/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/language",
"description": "ZenStack ZModel language specification",
"version": "3.0.0-beta.8",
"version": "3.0.0-beta.9",
"license": "MIT",
"author": "ZenStack Team",
"files": [
Expand All @@ -11,6 +11,7 @@
"type": "module",
"scripts": {
"build": "pnpm langium:generate && tsc --noEmit && tsup-node",
"watch": "tsup-node --watch",
"lint": "eslint src --ext ts",
"langium:generate": "langium generate",
"langium:generate:production": "langium generate --mode=production",
Expand Down
12 changes: 6 additions & 6 deletions packages/language/res/stdlib.zmodel
Original file line number Diff line number Diff line change
Expand Up @@ -543,22 +543,22 @@ attribute @upper() @@@targetField([StringField]) @@@validation
/**
* Validates a number field is greater than the given value.
*/
attribute @gt(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation
attribute @gt(_ value: Any, _ message: String?) @@@targetField([IntField, FloatField, DecimalField, BigIntField]) @@@validation

/**
* Validates a number field is greater than or equal to the given value.
*/
attribute @gte(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation
attribute @gte(_ value: Any, _ message: String?) @@@targetField([IntField, FloatField, DecimalField, BigIntField]) @@@validation

/**
* Validates a number field is less than the given value.
*/
attribute @lt(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation
attribute @lt(_ value: Any, _ message: String?) @@@targetField([IntField, FloatField, DecimalField, BigIntField]) @@@validation

/**
* Validates a number field is less than or equal to the given value.
*/
attribute @lte(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation
attribute @lte(_ value: Any, _ message: String?) @@@targetField([IntField, FloatField, DecimalField, BigIntField]) @@@validation

/**
* Validates the entity with a complex condition.
Expand Down Expand Up @@ -676,7 +676,7 @@ attribute @@allow(_ operation: String @@@completionHint(["'create'", "'read'", "
* @param condition: a boolean expression that controls if the operation should be allowed.
* @param override: a boolean value that controls if the field-level policy should override the model-level policy.
*/
attribute @allow(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'all'"]), _ condition: Boolean, _ override: Boolean?)
// attribute @allow(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'all'"]), _ condition: Boolean, _ override: Boolean?)

/**
* Defines an access policy that denies a set of operations when the given condition is true.
Expand All @@ -692,7 +692,7 @@ attribute @@deny(_ operation: String @@@completionHint(["'create'", "'read'", "'
* @param operation: comma-separated list of "create", "read", "update", "delete". Use "all" to denote all operations.
* @param condition: a boolean expression that controls if the operation should be denied.
*/
attribute @deny(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'all'"]), _ condition: Boolean)
// attribute @deny(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'all'"]), _ condition: Boolean)

/**
* Checks if the current user can perform the given operation on the given field.
Expand Down
6 changes: 3 additions & 3 deletions packages/language/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class DocumentLoadError extends Error {

export async function loadDocument(
fileName: string,
pluginModelFiles: string[] = [],
additionalModelFiles: string[] = [],
): Promise<
{ success: true; model: Model; warnings: string[] } | { success: false; errors: string[]; warnings: string[] }
> {
Expand Down Expand Up @@ -50,9 +50,9 @@ export async function loadDocument(
URI.file(path.resolve(path.join(_dirname, '../res', STD_LIB_MODULE_NAME))),
);

// load plugin model files
// load additional model files
const pluginDocs = await Promise.all(
pluginModelFiles.map((file) =>
additionalModelFiles.map((file) =>
services.shared.workspace.LangiumDocuments.getOrCreateDocument(URI.file(path.resolve(file))),
),
);
Expand Down
7 changes: 4 additions & 3 deletions packages/language/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,11 @@ export function getRecursiveBases(
return result;
}
seen.add(decl);
decl.mixins.forEach((mixin) => {
// avoid using mixin.ref since this function can be called before linking
const bases = [...decl.mixins, ...(isDataModel(decl) && decl.baseModel ? [decl.baseModel] : [])];
bases.forEach((base) => {
// avoid using .ref since this function can be called before linking
const baseDecl = decl.$container.declarations.find(
(d): d is TypeDef => isTypeDef(d) && d.name === mixin.$refText,
(d): d is TypeDef | DataModel => isTypeDef(d) || (isDataModel(d) && d.name === base.$refText),
);
if (baseDecl) {
if (!includeDelegate && isDelegateModel(baseDecl)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import {
getAllAttributes,
getStringLiteral,
hasAttribute,
isAuthOrAuthMemberAccess,
isBeforeInvocation,
isCollectionPredicate,
Expand Down Expand Up @@ -364,6 +365,11 @@ function assignableToAttributeParam(arg: AttributeArg, param: AttributeParam, at
if (dstType === 'ContextType') {
// ContextType is inferred from the attribute's container's type
if (isDataField(attr.$container)) {
// If the field is Typed JSON, and the attribute is @default, the argument must be a string
const dstIsTypedJson = hasAttribute(attr.$container, '@json');
if (dstIsTypedJson && attr.decl.ref?.name === '@default') {
return argResolvedType.decl === 'String';
}
dstIsArray = attr.$container.type.array;
}
}
Expand Down
25 changes: 19 additions & 6 deletions packages/language/src/validators/function-invocation-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,7 @@ export default class FunctionInvocationValidator implements AstValidator<Express
}

// validate the context allowed for the function
const exprContext = match(containerAttribute?.decl.$refText)
.with('@default', () => ExpressionContext.DefaultValue)
.with(P.union('@@allow', '@@deny', '@allow', '@deny'), () => ExpressionContext.AccessPolicy)
.with('@@validate', () => ExpressionContext.ValidationRule)
.with('@@index', () => ExpressionContext.Index)
.otherwise(() => undefined);
const exprContext = this.getExpressionContext(containerAttribute);

// get the context allowed for the function
const funcAllowedContext = getFunctionExpressionContext(funcDecl);
Expand Down Expand Up @@ -103,6 +98,24 @@ export default class FunctionInvocationValidator implements AstValidator<Express
}
}

private getExpressionContext(containerAttribute: DataModelAttribute | DataFieldAttribute | undefined) {
if (!containerAttribute) {
return undefined;
}
if (this.isValidationAttribute(containerAttribute)) {
return ExpressionContext.ValidationRule;
}
return match(containerAttribute?.decl.$refText)
.with('@default', () => ExpressionContext.DefaultValue)
.with(P.union('@@allow', '@@deny', '@allow', '@deny'), () => ExpressionContext.AccessPolicy)
.with('@@index', () => ExpressionContext.Index)
.otherwise(() => undefined);
}

private isValidationAttribute(attr: DataModelAttribute | DataFieldAttribute) {
return !!attr.decl.ref?.attributes.some((attr) => attr.decl.$refText === '@@@validation');
}

private validateArgs(funcDecl: FunctionDecl, args: Argument[], accept: ValidationAcceptor) {
let success = true;
for (let i = 0; i < funcDecl.params.length; i++) {
Expand Down
4 changes: 2 additions & 2 deletions packages/language/src/zmodel-scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ import {
getAuthDecl,
getRecursiveBases,
isAuthInvocation,
isCollectionPredicate,
isBeforeInvocation,
isCollectionPredicate,
resolveImportUri,
} from './utils';

Expand Down Expand Up @@ -75,7 +75,7 @@ export class ZModelScopeComputation extends DefaultScopeComputation {

override processNode(node: AstNode, document: LangiumDocument<AstNode>, scopes: PrecomputedScopes) {
super.processNode(node, document, scopes);
if (isDataModel(node)) {
if (isDataModel(node) || isTypeDef(node)) {
// add base fields to the scope recursively
const bases = getRecursiveBases(node);
for (const base of bases) {
Expand Down
4 changes: 4 additions & 0 deletions packages/plugins/policy/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import config from '@zenstackhq/eslint-config/base.js';

/** @type {import("eslint").Linter.Config} */
export default config;
Loading
Loading