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
3 changes: 2 additions & 1 deletion .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ jobs:
strategy:
matrix:
node-version: [20.x]
provider: [sqlite, postgresql]

steps:
- name: Checkout
Expand Down Expand Up @@ -76,4 +77,4 @@ jobs:
run: pnpm run lint

- name: Test
run: pnpm run test
run: TEST_DB_PROVIDER=${{ matrix.provider }} pnpm run test
5 changes: 2 additions & 3 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,8 @@
- [ ] Validation
- [ ] Access Policy
- [ ] Short-circuit pre-create check for scalar-field only policies
- [ ] Inject "replace into"
- [ ] Inject "on conflict do update"
- [ ] Inject "insert into select from"
- [x] Inject "on conflict do update"
- [x] `check` function
- [x] Migration
- [ ] Databases
- [x] SQLite
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"name": "zenstack-v3",
"version": "3.0.0-beta.4",
"version": "3.0.0-beta.5",
"description": "ZenStack",
"packageManager": "[email protected]",
"scripts": {
"build": "turbo run build",
"watch": "turbo run watch build",
"lint": "turbo run lint",
"test": "turbo run test",
"test": "vitest run",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"pr": "gh pr create --fill-first --base dev",
"merge-main": "gh pr create --title \"merge dev to main\" --body \"\" --base main --head dev",
Expand Down
2 changes: 1 addition & 1 deletion 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.4",
"version": "3.0.0-beta.5",
"type": "module",
"author": {
"name": "ZenStack Team"
Expand Down
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.4",
"version": "3.0.0-beta.5",
"description": "ZenStack Common Helpers",
"type": "module",
"scripts": {
Expand Down
1 change: 1 addition & 0 deletions packages/common-helpers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './param-case';
export * from './sleep';
export * from './tiny-invariant';
export * from './upper-case-first';
export * from './zip';
11 changes: 11 additions & 0 deletions packages/common-helpers/src/zip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Zips two arrays into an array of tuples.
*/
export function zip<T, U>(arr1: T[], arr2: U[]): Array<[T, U]> {
const length = Math.min(arr1.length, arr2.length);
const result: Array<[T, U]> = [];
for (let i = 0; i < length; i++) {
result.push([arr1[i]!, arr2[i]!]);
}
return result;
}
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.4",
"version": "3.0.0-beta.5",
"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.4",
"version": "3.0.0-beta.5",
"description": "Kysely dialect for sql.js",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/eslint-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/eslint-config",
"version": "3.0.0-beta.4",
"version": "3.0.0-beta.5",
"type": "module",
"private": true,
"license": "MIT"
Expand Down
2 changes: 1 addition & 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.4",
"version": "3.0.0-beta.5",
"license": "MIT",
"author": "ZenStack Team",
"files": [
Expand Down
30 changes: 14 additions & 16 deletions packages/language/res/stdlib.zmodel
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,10 @@ function future(): Any {
} @@@expressionContext([AccessPolicy])

/**
* If the field value contains the search string. By default, the search is case-sensitive,
* but you can override the behavior with the "caseInSensitive" argument.
* Checks if the field value contains the search string. By default, the search is case-sensitive, and
* "LIKE" operator is used to match. If `caseInSensitive` is true, "ILIKE" operator is used if
* supported, otherwise it still falls back to "LIKE" and delivers whatever the database's
* behavior is.
*/
function contains(field: String, search: String, caseInSensitive: Boolean?): Boolean {
} @@@expressionContext([AccessPolicy, ValidationRule])
Expand All @@ -136,15 +138,21 @@ function search(field: String, search: String): Boolean {
} @@@expressionContext([AccessPolicy])

/**
* If the field value starts with the search string
* Checks the field value starts with the search string. By default, the search is case-sensitive, and
* "LIKE" operator is used to match. If `caseInSensitive` is true, "ILIKE" operator is used if
* supported, otherwise it still falls back to "LIKE" and delivers whatever the database's
* behavior is.
*/
function startsWith(field: String, search: String): Boolean {
function startsWith(field: String, search: String, caseInSensitive: Boolean?): Boolean {
} @@@expressionContext([AccessPolicy, ValidationRule])

/**
* If the field value ends with the search string
* Checks if the field value ends with the search string. By default, the search is case-sensitive, and
* "LIKE" operator is used to match. If `caseInSensitive` is true, "ILIKE" operator is used if
* supported, otherwise it still falls back to "LIKE" and delivers whatever the database's
* behavior is.
*/
function endsWith(field: String, search: String): Boolean {
function endsWith(field: String, search: String, caseInSensitive: Boolean?): Boolean {
} @@@expressionContext([AccessPolicy, ValidationRule])

/**
Expand Down Expand Up @@ -594,16 +602,6 @@ function datetime(field: String): Boolean {
function url(field: String): Boolean {
} @@@expressionContext([ValidationRule])

/**
* Checks if the current user can perform the given operation on the given field.
*
* @param field: The field to check access for
* @param operation: The operation to check access for. Can be "read", "create", "update", or "delete". If the operation is not provided,
* it defaults the operation of the containing policy rule.
*/
function check(field: Any, operation: String?): Boolean {
} @@@expressionContext([AccessPolicy])

//////////////////////////////////////////////
// End validation attributes and functions
//////////////////////////////////////////////
Expand Down
3 changes: 2 additions & 1 deletion packages/language/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,9 @@ export function getFieldReference(expr: Expression): DataField | undefined {
}
}

// TODO: move to policy plugin
export function isCheckInvocation(node: AstNode) {
return isInvocationExpr(node) && node.function.ref?.name === 'check' && isFromStdlib(node.function.ref);
return isInvocationExpr(node) && node.function.ref?.name === 'check';
}

export function resolveTransitiveImports(documents: LangiumDocuments, model: Model) {
Expand Down
18 changes: 9 additions & 9 deletions packages/language/src/validators/expression-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,18 +108,21 @@ export default class ExpressionValidator implements AstValidator<Expression> {
supportedShapes = ['Boolean', 'Any'];
}

const leftResolvedDecl = expr.left.$resolvedType?.decl;
const rightResolvedDecl = expr.right.$resolvedType?.decl;

if (
typeof expr.left.$resolvedType?.decl !== 'string' ||
!supportedShapes.includes(expr.left.$resolvedType.decl)
leftResolvedDecl &&
(typeof leftResolvedDecl !== 'string' || !supportedShapes.includes(leftResolvedDecl))
) {
accept('error', `invalid operand type for "${expr.operator}" operator`, {
node: expr.left,
});
return;
}
if (
typeof expr.right.$resolvedType?.decl !== 'string' ||
!supportedShapes.includes(expr.right.$resolvedType.decl)
rightResolvedDecl &&
(typeof rightResolvedDecl !== 'string' || !supportedShapes.includes(rightResolvedDecl))
) {
accept('error', `invalid operand type for "${expr.operator}" operator`, {
node: expr.right,
Expand All @@ -128,14 +131,11 @@ export default class ExpressionValidator implements AstValidator<Expression> {
}

// DateTime comparison is only allowed between two DateTime values
if (expr.left.$resolvedType.decl === 'DateTime' && expr.right.$resolvedType.decl !== 'DateTime') {
if (leftResolvedDecl === 'DateTime' && rightResolvedDecl && rightResolvedDecl !== 'DateTime') {
accept('error', 'incompatible operand types', {
node: expr,
});
} else if (
expr.right.$resolvedType.decl === 'DateTime' &&
expr.left.$resolvedType.decl !== 'DateTime'
) {
} else if (rightResolvedDecl === 'DateTime' && leftResolvedDecl && leftResolvedDecl !== 'DateTime') {
accept('error', 'incompatible operand types', {
node: expr,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ export default class FunctionInvocationValidator implements AstValidator<Express
return true;
}

// TODO: move this to policy plugin
@func('check')
// @ts-expect-error
private _checkCheck(expr: InvocationExpr, accept: ValidationAcceptor) {
Expand Down
25 changes: 25 additions & 0 deletions packages/language/test/delegate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import { loadSchema, loadSchemaWithError } from './utils';
describe('Delegate Tests', () => {
it('supports inheriting from delegate', async () => {
const model = await loadSchema(`
datasource db {
provider = 'sqlite'
url = 'file:./dev.db'
}

model A {
id Int @id @default(autoincrement())
x String
Expand All @@ -24,6 +29,11 @@ describe('Delegate Tests', () => {
it('rejects inheriting from non-delegate models', async () => {
await loadSchemaWithError(
`
datasource db {
provider = 'sqlite'
url = 'file:./dev.db'
}

model A {
id Int @id @default(autoincrement())
x String
Expand All @@ -40,6 +50,11 @@ describe('Delegate Tests', () => {
it('can detect cyclic inherits', async () => {
await loadSchemaWithError(
`
datasource db {
provider = 'sqlite'
url = 'file:./dev.db'
}

model A extends B {
x String
@@delegate(x)
Expand All @@ -57,6 +72,11 @@ describe('Delegate Tests', () => {
it('can detect duplicated fields from base model', async () => {
await loadSchemaWithError(
`
datasource db {
provider = 'sqlite'
url = 'file:./dev.db'
}

model A {
id String @id
x String
Expand All @@ -74,6 +94,11 @@ describe('Delegate Tests', () => {
it('can detect duplicated attributes from base model', async () => {
await loadSchemaWithError(
`
datasource db {
provider = 'sqlite'
url = 'file:./dev.db'
}

model A {
id String @id
x String
Expand Down
21 changes: 19 additions & 2 deletions packages/language/test/import.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ describe('Import tests', () => {
fs.writeFileSync(
path.join(name, 'a.zmodel'),
`
datasource db {
provider = 'sqlite'
url = 'file:./dev.db'
}

model A {
id Int @id
name String
Expand Down Expand Up @@ -48,6 +53,12 @@ enum Role {
path.join(name, 'b.zmodel'),
`
import './a'

datasource db {
provider = 'sqlite'
url = 'file:./dev.db'
}

model User {
id Int @id
role Role
Expand All @@ -56,7 +67,7 @@ model User {
);

const model = await expectLoaded(path.join(name, 'b.zmodel'));
expect((model.declarations[0] as DataModel).fields[1].type.reference?.ref?.name).toBe('Role');
expect((model.declarations[1] as DataModel).fields[1].type.reference?.ref?.name).toBe('Role');
});

it('supports cyclic imports', async () => {
Expand All @@ -65,6 +76,12 @@ model User {
path.join(name, 'a.zmodel'),
`
import './b'

datasource db {
provider = 'sqlite'
url = 'file:./dev.db'
}

model A {
id Int @id
b B?
Expand All @@ -86,7 +103,7 @@ model B {
const modelB = await expectLoaded(path.join(name, 'b.zmodel'));
expect((modelB.declarations[0] as DataModel).fields[1].type.reference?.ref?.name).toBe('A');
const modelA = await expectLoaded(path.join(name, 'a.zmodel'));
expect((modelA.declarations[0] as DataModel).fields[1].type.reference?.ref?.name).toBe('B');
expect((modelA.declarations[1] as DataModel).fields[1].type.reference?.ref?.name).toBe('B');
});

async function expectLoaded(file: string) {
Expand Down
15 changes: 15 additions & 0 deletions packages/language/test/mixin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import { DataModel, TypeDef } from '../src/ast';
describe('Mixin Tests', () => {
it('supports model mixing types to Model', async () => {
const model = await loadSchema(`
datasource db {
provider = 'sqlite'
url = 'file:./dev.db'
}

type A {
x String
}
Expand All @@ -25,6 +30,11 @@ describe('Mixin Tests', () => {

it('supports model mixing types to type', async () => {
const model = await loadSchema(`
datasource db {
provider = 'sqlite'
url = 'file:./dev.db'
}

type A {
x String
}
Expand Down Expand Up @@ -52,6 +62,11 @@ describe('Mixin Tests', () => {
it('can detect cyclic mixins', async () => {
await loadSchemaWithError(
`
datasource db {
provider = 'sqlite'
url = 'file:./dev.db'
}

type A with B {
x String
}
Expand Down
Loading