Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
  •  
  •  
  •  
5 changes: 2 additions & 3 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@
- [x] Array update
- [x] Strict typing for checked/unchecked input
- [x] Upsert
- [ ] Implement with "on conflict"
- [x] Delete
- [x] Aggregation
- [x] Count
Expand Down Expand Up @@ -86,7 +85,7 @@
- [ ] Global omit
- [ ] DbNull vs JsonNull
- [ ] Migrate to tsdown
- [ ] @default validation
- [x] @default validation
- [ ] Benchmark
- [x] Plugin
- [x] Post-mutation hooks should be called after transaction is committed
Expand All @@ -96,7 +95,7 @@
- [x] ZModel
- [x] Runtime
- [x] Typing
- [ ] Validation
- [x] Validation
- [ ] Access Policy
- [ ] Short-circuit pre-create check for scalar-field only policies
- [x] Inject "on conflict do update"
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-v3",
"version": "3.0.0-beta.8",
"version": "3.0.0-beta.9",
"description": "ZenStack",
"packageManager": "pnpm@10.12.1",
"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
27 changes: 14 additions & 13 deletions packages/language/res/stdlib.zmodel
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ enum AttributeTargetField {
BytesField
ModelField
TypeDefField
ListField
}

/**
Expand Down Expand Up @@ -486,9 +487,9 @@ attribute @db.ByteA() @@@targetField([BytesField]) @@@prisma
//////////////////////////////////////////////

/**
* Validates length of a string field.
* Validates length of a string field or list field.
*/
attribute @length(_ min: Int?, _ max: Int?, _ message: String?) @@@targetField([StringField]) @@@validation
attribute @length(_ min: Int?, _ max: Int?, _ message: String?) @@@targetField([StringField, ListField]) @@@validation

/**
* Validates a string field value starts with the given text.
Expand Down Expand Up @@ -543,32 +544,32 @@ 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.
*/
attribute @@validate(_ value: Boolean, _ message: String?, _ path: String[]?) @@@validation

/**
* Validates length of a string field.
* Returns the length of a string field or a list field.
*/
function length(field: String, min: Int, max: Int?): Boolean {
function length(field: Any): Int {
} @@@expressionContext([ValidationRule])


Expand All @@ -581,19 +582,19 @@ function regex(field: String, regex: String): Boolean {
/**
* Validates a string field value is a valid email address.
*/
function email(field: String): Boolean {
function isEmail(field: String): Boolean {
} @@@expressionContext([ValidationRule])

/**
* Validates a string field value is a valid ISO datetime.
*/
function datetime(field: String): Boolean {
function isDateTime(field: String): Boolean {
} @@@expressionContext([ValidationRule])

/**
* Validates a string field value is a valid url.
*/
function url(field: String): Boolean {
function isUrl(field: String): Boolean {
} @@@expressionContext([ValidationRule])

//////////////////////////////////////////////
Expand Down Expand Up @@ -676,7 +677,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 +693,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 Expand Up @@ -485,6 +491,9 @@ function isValidAttributeTarget(attrDecl: Attribute, targetDecl: DataField) {
case 'TypeDefField':
allowed = allowed || isTypeDef(targetDecl.type.reference?.ref);
break;
case 'ListField':
allowed = allowed || (!isDataModel(targetDecl.type.reference?.ref) && targetDecl.type.array);
break;
default:
break;
}
Expand Down
Loading
Loading