Skip to content
This repository was archived by the owner on Mar 1, 2026. It is now read-only.

Commit 880d7e4

Browse files
authored
Merge pull request #636 from zenstackhq/dev
merge dev to main (v3.3.0)
2 parents d150edb + b3aff66 commit 880d7e4

179 files changed

Lines changed: 10382 additions & 3403 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build-test.yml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,23 @@ jobs:
3131
ports:
3232
- 5432:5432
3333

34+
mysql:
35+
image: mysql:8.4
36+
env:
37+
MYSQL_ROOT_PASSWORD: mysql
38+
ports:
39+
- 3306:3306
40+
# Set health checks to wait until mysql has started
41+
options: >-
42+
--health-cmd="mysqladmin ping --silent"
43+
--health-interval=10s
44+
--health-timeout=5s
45+
--health-retries=3
46+
3447
strategy:
3548
matrix:
3649
node-version: [22.x]
37-
provider: [sqlite, postgresql]
50+
provider: [sqlite, postgresql, mysql]
3851

3952
steps:
4053
- name: Checkout
@@ -81,5 +94,9 @@ jobs:
8194
- name: Lint
8295
run: pnpm run lint
8396

97+
- name: Set MySQL max_connections
98+
run: |
99+
mysql -h 127.0.0.1 -uroot -pmysql -e "SET GLOBAL max_connections=500;"
100+
84101
- name: Test
85102
run: TEST_DB_PROVIDER=${{ matrix.provider }} pnpm run test

TODO.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
- [ ] ZModel
1919
- [x] Import
2020
- [ ] View support
21-
- [ ] Datasource provider-scoped attributes
2221
- [ ] ORM
2322
- [x] Create
2423
- [x] Input validation
@@ -72,7 +71,7 @@
7271
- [x] Query builder API
7372
- [x] Computed fields
7473
- [x] Plugin
75-
- [ ] Custom procedures
74+
- [x] Custom procedures
7675
- [ ] Misc
7776
- [x] JSDoc for CRUD methods
7877
- [x] Cache validation schemas
@@ -110,4 +109,4 @@
110109
- [x] SQLite
111110
- [x] PostgreSQL
112111
- [x] Multi-schema
113-
- [ ] MySQL
112+
- [x] MySQL

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "zenstack-v3",
3-
"version": "3.2.1",
3+
"version": "3.3.0",
44
"description": "ZenStack",
55
"packageManager": "pnpm@10.23.0",
66
"type": "module",
@@ -9,8 +9,9 @@
99
"watch": "turbo run watch build",
1010
"lint": "turbo run lint",
1111
"test": "turbo run test",
12-
"test:all": "pnpm run test:sqlite && pnpm run test:pg",
12+
"test:all": "pnpm run test:sqlite && pnpm run test:pg && pnpm run test:mysql",
1313
"test:pg": "TEST_DB_PROVIDER=postgresql turbo run test",
14+
"test:mysql": "TEST_DB_PROVIDER=mysql turbo run test",
1415
"test:sqlite": "TEST_DB_PROVIDER=sqlite turbo run test",
1516
"test:coverage": "vitest run --coverage",
1617
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
@@ -19,6 +20,7 @@
1920
"bump-patch": "gh workflow run .github/workflows/bump-version.yml --ref dev -f version_type=patch",
2021
"bump-minor": "gh workflow run .github/workflows/bump-version.yml --ref dev -f version_type=minor",
2122
"publish-all": "pnpm --filter \"./packages/**\" -r publish --access public",
23+
"publish-canary": "pnpm --filter \"./packages/**\" -r publish --access public --tag canary --no-git-checks",
2224
"publish-preview": "pnpm --filter \"./packages/**\" -r publish --force --registry https://preview.registry.zenstack.dev/",
2325
"unpublish-preview": "pnpm --filter \"./packages/**\" -r --shell-mode exec -- npm unpublish -f --registry https://preview.registry.zenstack.dev/ \"\\$PNPM_PACKAGE_NAME\""
2426
},

packages/auth-adapters/better-auth/package.json

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
{
22
"name": "@zenstackhq/better-auth",
3-
"version": "3.2.1",
3+
"version": "3.3.0",
44
"description": "ZenStack Better Auth Adapter. This adapter is modified from better-auth's Prisma adapter.",
55
"type": "module",
66
"scripts": {
77
"build": "tsc --noEmit && tsup-node",
88
"watch": "tsup-node --watch",
99
"lint": "eslint src --ext ts",
10+
"test": "vitest run",
1011
"pack": "pnpm pack"
1112
},
1213
"keywords": [
@@ -45,10 +46,14 @@
4546
"better-auth": "^1.3.0"
4647
},
4748
"devDependencies": {
48-
"@better-auth/core": "^1.3.0",
49-
"better-auth": "^1.3.0",
49+
"@better-auth/core": "1.4.17",
50+
"better-auth": "1.4.17",
51+
"@better-auth/cli": "1.4.17",
52+
"@types/tmp": "catalog:",
53+
"@zenstackhq/cli": "workspace:*",
5054
"@zenstackhq/eslint-config": "workspace:*",
5155
"@zenstackhq/typescript-config": "workspace:*",
52-
"@zenstackhq/vitest-config": "workspace:*"
56+
"@zenstackhq/vitest-config": "workspace:*",
57+
"tmp": "catalog:"
5358
}
5459
}

packages/auth-adapters/better-auth/src/schema-generator.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
InvocationExpr,
1717
isDataModel,
1818
Model,
19+
NumberLiteral,
1920
ReferenceExpr,
2021
StringLiteral,
2122
} from '@zenstackhq/language/ast';
@@ -259,6 +260,13 @@ function getMappedFieldType({ bigint, type }: DBFieldAttribute) {
259260
.with('json', () => ({ type: 'Json' }))
260261
.with('string[]', () => ({ type: 'String', array: true }))
261262
.with('number[]', () => ({ type: 'Int', array: true }))
263+
.when(
264+
(v) => Array.isArray(v) && v.every((e) => typeof e === 'string'),
265+
() => {
266+
// Handle enum types (e.g., ['user', 'admin']), map them to String type for now
267+
return { type: 'String' };
268+
},
269+
)
262270
.otherwise(() => {
263271
throw new Error(`Unsupported field type: ${type}`);
264272
});
@@ -332,6 +340,10 @@ function addOrUpdateModel(
332340
addDefaultNow(df);
333341
} else if (typeof field.defaultValue === 'boolean') {
334342
addFieldAttribute(df, '@default', [createBooleanAttributeArg(field.defaultValue)]);
343+
} else if (typeof field.defaultValue === 'string') {
344+
addFieldAttribute(df, '@default', [createStringAttributeArg(field.defaultValue)]);
345+
} else if (typeof field.defaultValue === 'number') {
346+
addFieldAttribute(df, '@default', [createNumberAttributeArg(field.defaultValue)]);
335347
} else if (typeof field.defaultValue === 'function') {
336348
// For other function-based defaults, we'll need to check what they return
337349
const defaultVal = field.defaultValue();
@@ -537,6 +549,19 @@ function createBooleanAttributeArg(value: boolean) {
537549
return arg;
538550
}
539551

552+
function createNumberAttributeArg(value: number) {
553+
const arg: AttributeArg = {
554+
$type: 'AttributeArg',
555+
} as any;
556+
const expr: NumberLiteral = {
557+
$type: 'NumberLiteral',
558+
value: value.toString(),
559+
$container: arg,
560+
};
561+
arg.value = expr;
562+
return arg;
563+
}
564+
540565
function createStringAttributeArg(value: string) {
541566
const arg: AttributeArg = {
542567
$type: 'AttributeArg',
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { zenstackAdapter } from '../src/adapter';
2+
import { betterAuth } from 'better-auth';
3+
4+
export const auth = betterAuth({
5+
database: zenstackAdapter({} as any, {
6+
provider: 'postgresql',
7+
}),
8+
user: {
9+
additionalFields: {
10+
role: {
11+
type: ['user', 'admin'],
12+
required: false,
13+
defaultValue: 'user',
14+
input: false, // don't allow user to set role
15+
},
16+
lang: {
17+
type: 'string',
18+
required: false,
19+
defaultValue: 'en',
20+
},
21+
age: {
22+
type: 'number',
23+
required: true,
24+
defaultValue: 18,
25+
},
26+
admin: {
27+
type: 'boolean',
28+
required: false,
29+
defaultValue: false,
30+
},
31+
},
32+
},
33+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { zenstackAdapter } from '../src/adapter';
2+
import { betterAuth } from 'better-auth';
3+
4+
export const auth = betterAuth({
5+
database: zenstackAdapter({} as any, {
6+
provider: 'postgresql',
7+
}),
8+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { execSync } from 'node:child_process';
2+
import fs from 'node:fs';
3+
import path from 'node:path';
4+
import { describe, expect, it } from 'vitest';
5+
import { fileURLToPath } from 'node:url';
6+
import tmp from 'tmp';
7+
8+
const __filename = fileURLToPath(import.meta.url);
9+
const __dirname = path.dirname(__filename);
10+
11+
/**
12+
* Helper function to generate schema using better-auth CLI
13+
*/
14+
function generateSchema(configFile: string): string {
15+
const { name: workDir } = tmp.dirSync({ unsafeCleanup: true });
16+
const schemaPath = path.join(workDir, 'schema.zmodel');
17+
const configPath = path.join(__dirname, configFile);
18+
19+
execSync(`pnpm better-auth generate --config ${configPath} --output ${schemaPath} --yes`, {
20+
cwd: __dirname,
21+
stdio: 'pipe',
22+
});
23+
24+
return schemaPath;
25+
}
26+
27+
/**
28+
* Helper function to verify schema with zenstack check
29+
*/
30+
function verifySchema(schemaPath: string) {
31+
const cliPath = path.join(__dirname, '../../../cli/dist/index.js');
32+
const workDir = path.dirname(schemaPath);
33+
34+
expect(fs.existsSync(schemaPath)).toBe(true);
35+
36+
expect(() => {
37+
execSync(`node ${cliPath} check --schema ${schemaPath}`, {
38+
cwd: workDir,
39+
stdio: 'pipe',
40+
});
41+
}).not.toThrow();
42+
}
43+
44+
describe('Cli schema generation tests', () => {
45+
it('works with simple config', async () => {
46+
const schemaPath = generateSchema('auth.ts');
47+
verifySchema(schemaPath);
48+
});
49+
50+
it('works with custom config', async () => {
51+
const schemaPath = generateSchema('auth-custom.ts');
52+
verifySchema(schemaPath);
53+
54+
// Verify that the generated schema contains the expected default values
55+
const schemaContent = fs.readFileSync(schemaPath, 'utf-8');
56+
expect(schemaContent).toMatch(/role\s+String/);
57+
expect(schemaContent).toContain("@default('user')");
58+
expect(schemaContent).toMatch(/lang\s+String/);
59+
expect(schemaContent).toContain("@default('en')");
60+
expect(schemaContent).toMatch(/age\s+Int/);
61+
expect(schemaContent).toContain('@default(18)');
62+
expect(schemaContent).toMatch(/admin\s+Boolean/);
63+
expect(schemaContent).toContain('@default(false)');
64+
});
65+
});

packages/cli/package.json

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"publisher": "zenstack",
44
"displayName": "ZenStack CLI",
55
"description": "FullStack database toolkit with built-in access control and automatic API generation.",
6-
"version": "3.2.1",
6+
"version": "3.3.0",
77
"type": "module",
88
"author": {
99
"name": "ZenStack Team"
@@ -38,30 +38,39 @@
3838
"dependencies": {
3939
"@zenstackhq/common-helpers": "workspace:*",
4040
"@zenstackhq/language": "workspace:*",
41+
"@zenstackhq/orm": "workspace:*",
4142
"@zenstackhq/sdk": "workspace:*",
43+
"@zenstackhq/server": "workspace:*",
44+
"better-sqlite3": "catalog:",
4245
"chokidar": "^5.0.0",
4346
"colors": "1.4.0",
4447
"commander": "^8.3.0",
48+
"cors": "^2.8.5",
49+
"dotenv": "^17.2.3",
4550
"execa": "^9.6.0",
51+
"express": "^5.0.0",
4652
"jiti": "^2.6.1",
4753
"langium": "catalog:",
4854
"mixpanel": "^0.18.1",
55+
"mysql2": "catalog:",
4956
"ora": "^5.4.1",
5057
"package-manager-detector": "^1.3.0",
58+
"pg": "catalog:",
5159
"prisma": "catalog:",
5260
"semver": "^7.7.2",
5361
"ts-pattern": "catalog:"
5462
},
5563
"devDependencies": {
5664
"@types/better-sqlite3": "catalog:",
65+
"@types/cors": "^2.8.19",
66+
"@types/express": "^5.0.0",
67+
"@types/pg": "^8.16.0",
5768
"@types/semver": "^7.7.0",
5869
"@types/tmp": "catalog:",
5970
"@zenstackhq/eslint-config": "workspace:*",
60-
"@zenstackhq/orm": "workspace:*",
6171
"@zenstackhq/testtools": "workspace:*",
6272
"@zenstackhq/typescript-config": "workspace:*",
6373
"@zenstackhq/vitest-config": "workspace:*",
64-
"better-sqlite3": "catalog:",
6574
"tmp": "catalog:"
6675
},
6776
"engines": {

packages/cli/src/actions/action-utils.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,15 @@ export async function requireDataSourceUrl(schemaFile: string) {
144144
throw new CliError('The schema\'s "datasource" must have a "url" field to use this command.');
145145
}
146146
}
147+
148+
export function getOutputPath(options: { output?: string }, schemaFile: string) {
149+
if (options.output) {
150+
return options.output;
151+
}
152+
const pkgJsonConfig = getPkgJsonConfig(process.cwd());
153+
if (pkgJsonConfig.output) {
154+
return pkgJsonConfig.output;
155+
} else {
156+
return path.dirname(schemaFile);
157+
}
158+
}

0 commit comments

Comments
 (0)