Skip to content

Commit d67085e

Browse files
authored
merge dev to main (v3.4.0) (#2414)
2 parents f6fc8a6 + 4b42ed9 commit d67085e

File tree

144 files changed

+14100
-4977
lines changed

Some content is hidden

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

144 files changed

+14100
-4977
lines changed

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
2020
### Testing
2121

2222
- E2E tests are in `tests/e2e/` directory
23+
- Regression tests for GitHub issues go in `tests/regression/test/` as `issue-{number}.test.ts`
2324

2425
### ZenStack CLI Commands
2526

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "zenstack-v3",
3-
"version": "3.3.3",
3+
"version": "3.4.0",
44
"description": "ZenStack",
55
"packageManager": "pnpm@10.23.0",
66
"type": "module",
@@ -50,6 +50,11 @@
5050
"better-sqlite3",
5151
"esbuild",
5252
"vue-demi"
53-
]
53+
],
54+
"overrides": {
55+
"cookie@<0.7.0": ">=0.7.0",
56+
"lodash-es@>=4.0.0 <=4.17.22": ">=4.17.23",
57+
"lodash@>=4.0.0 <=4.17.22": ">=4.17.23"
58+
}
5459
}
5560
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenstackhq/better-auth",
3-
"version": "3.3.3",
3+
"version": "3.4.0",
44
"description": "ZenStack Better Auth Adapter. This adapter is modified from better-auth's Prisma adapter.",
55
"type": "module",
66
"scripts": {
@@ -46,9 +46,9 @@
4646
"better-auth": "^1.3.0"
4747
},
4848
"devDependencies": {
49-
"@better-auth/core": "1.4.17",
50-
"better-auth": "1.4.17",
51-
"@better-auth/cli": "1.4.17",
49+
"@better-auth/core": "1.4.19",
50+
"better-auth": "1.4.19",
51+
"@better-auth/cli": "1.4.19",
5252
"@types/tmp": "catalog:",
5353
"@zenstackhq/cli": "workspace:*",
5454
"@zenstackhq/eslint-config": "workspace:*",

packages/auth-adapters/better-auth/src/adapter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ export const zenstackAdapter = <Schema extends SchemaDef>(db: ClientContract<Sch
171171
const whereClause = convertWhereClause(model, where);
172172
return await modelDb.update({
173173
where: whereClause,
174-
data: update as UpdateInput<SchemaDef, GetModels<SchemaDef>>,
174+
data: update as UpdateInput<SchemaDef, GetModels<SchemaDef>, any>,
175175
});
176176
},
177177

packages/cli/package.json

Lines changed: 22 additions & 7 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.3.3",
6+
"version": "3.4.0",
77
"type": "module",
88
"author": {
99
"name": "ZenStack Team"
@@ -37,12 +37,11 @@
3737
},
3838
"dependencies": {
3939
"@zenstackhq/common-helpers": "workspace:*",
40-
"@zenstackhq/schema": "workspace:*",
4140
"@zenstackhq/language": "workspace:*",
4241
"@zenstackhq/orm": "workspace:*",
42+
"@zenstackhq/schema": "workspace:*",
4343
"@zenstackhq/sdk": "workspace:*",
4444
"@zenstackhq/server": "workspace:*",
45-
"better-sqlite3": "catalog:",
4645
"chokidar": "^5.0.0",
4746
"colors": "1.4.0",
4847
"commander": "^8.3.0",
@@ -53,13 +52,13 @@
5352
"jiti": "^2.6.1",
5453
"langium": "catalog:",
5554
"mixpanel": "^0.18.1",
56-
"mysql2": "catalog:",
5755
"ora": "^5.4.1",
5856
"package-manager-detector": "^1.3.0",
59-
"pg": "catalog:",
6057
"prisma": "catalog:",
6158
"semver": "^7.7.2",
62-
"ts-pattern": "catalog:"
59+
"terminal-link": "^5.0.0",
60+
"ts-pattern": "catalog:",
61+
"zod": "catalog:"
6362
},
6463
"devDependencies": {
6564
"@types/better-sqlite3": "catalog:",
@@ -74,7 +73,23 @@
7473
"@zenstackhq/vitest-config": "workspace:*",
7574
"tmp": "catalog:"
7675
},
76+
"peerDependencies": {
77+
"better-sqlite3": "catalog:",
78+
"mysql2": "catalog:",
79+
"pg": "catalog:"
80+
},
81+
"peerDependenciesMeta": {
82+
"pg": {
83+
"optional": true
84+
},
85+
"better-sqlite3": {
86+
"optional": true
87+
},
88+
"mysql2": {
89+
"optional": true
90+
}
91+
},
7792
"engines": {
7893
"node": ">=20"
7994
}
80-
}
95+
}

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import fs from 'node:fs';
66
import { createRequire } from 'node:module';
77
import path from 'node:path';
88
import { CliError } from '../cli-error';
9+
import terminalLink from 'terminal-link';
10+
import { z } from 'zod';
911

1012
export function getSchemaFile(file?: string) {
1113
if (file) {
@@ -216,3 +218,69 @@ export async function getZenStackPackages(
216218

217219
return result.filter((p) => !!p);
218220
}
221+
222+
const FETCH_CLI_MAX_TIME = 1000;
223+
const CLI_CONFIG_ENDPOINT = 'https://zenstack.dev/config/cli-v3.json';
224+
225+
const usageTipsSchema = z.object({
226+
notifications: z.array(z.object({ title: z.string(), url: z.url().optional(), active: z.boolean() })),
227+
});
228+
229+
/**
230+
* Starts the usage tips fetch in the background. Returns a callback that, when invoked check if the fetch
231+
* is complete. If not complete, it will wait until the max time is reached. After that, if fetch is still
232+
* not complete, just return.
233+
*/
234+
export function startUsageTipsFetch() {
235+
let fetchedData: z.infer<typeof usageTipsSchema> | undefined = undefined;
236+
let fetchComplete = false;
237+
238+
const start = Date.now();
239+
const controller = new AbortController();
240+
241+
fetch(CLI_CONFIG_ENDPOINT, {
242+
headers: { accept: 'application/json' },
243+
signal: controller.signal,
244+
})
245+
.then(async (res) => {
246+
if (!res.ok) return;
247+
const data = await res.json();
248+
const parseResult = usageTipsSchema.safeParse(data);
249+
if (parseResult.success) {
250+
fetchedData = parseResult.data;
251+
}
252+
})
253+
.catch(() => {
254+
// noop
255+
})
256+
.finally(() => {
257+
fetchComplete = true;
258+
});
259+
260+
return async () => {
261+
const elapsed = Date.now() - start;
262+
263+
if (!fetchComplete && elapsed < FETCH_CLI_MAX_TIME) {
264+
// wait for the timeout
265+
await new Promise((resolve) => setTimeout(resolve, FETCH_CLI_MAX_TIME - elapsed));
266+
}
267+
268+
if (!fetchComplete) {
269+
controller.abort();
270+
return;
271+
}
272+
273+
if (!fetchedData) return;
274+
275+
const activeItems = fetchedData.notifications.filter((item) => item.active);
276+
// show a random active item
277+
if (activeItems.length > 0) {
278+
const item = activeItems[Math.floor(Math.random() * activeItems.length)]!;
279+
if (item.url) {
280+
console.log(terminalLink(item.title, item.url));
281+
} else {
282+
console.log(item.title);
283+
}
284+
}
285+
};
286+
}

packages/cli/src/actions/generate.ts

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,36 @@
11
import { invariant, singleDebounce } from '@zenstackhq/common-helpers';
22
import { ZModelLanguageMetaData } from '@zenstackhq/language';
3-
import { type AbstractDeclaration, isPlugin, LiteralExpr, Plugin, type Model } from '@zenstackhq/language/ast';
3+
import { isPlugin, LiteralExpr, Plugin, type AbstractDeclaration, type Model } from '@zenstackhq/language/ast';
44
import { getLiteral, getLiteralArray } from '@zenstackhq/language/utils';
55
import { type CliPlugin } from '@zenstackhq/sdk';
6+
import { watch } from 'chokidar';
67
import colors from 'colors';
78
import { createJiti } from 'jiti';
89
import fs from 'node:fs';
910
import path from 'node:path';
1011
import { pathToFileURL } from 'node:url';
11-
import { watch } from 'chokidar';
1212
import ora, { type Ora } from 'ora';
13+
import semver from 'semver';
1314
import { CliError } from '../cli-error';
1415
import * as corePlugins from '../plugins';
15-
import { getOutputPath, getSchemaFile, getZenStackPackages, loadSchemaDocument } from './action-utils';
16-
import semver from 'semver';
16+
import {
17+
getOutputPath,
18+
getSchemaFile,
19+
getZenStackPackages,
20+
loadSchemaDocument,
21+
startUsageTipsFetch,
22+
} from './action-utils';
1723

1824
type Options = {
1925
schema?: string;
2026
output?: string;
2127
silent: boolean;
2228
watch: boolean;
23-
lite: boolean;
24-
liteOnly: boolean;
29+
lite?: boolean;
30+
liteOnly?: boolean;
31+
generateModels?: boolean;
32+
generateInput?: boolean;
33+
tips?: boolean;
2534
};
2635

2736
/**
@@ -33,8 +42,13 @@ export async function run(options: Options) {
3342
} catch (err) {
3443
console.warn(colors.yellow(`Failed to check for mismatched ZenStack packages: ${err}`));
3544
}
45+
46+
const maybeShowUsageTips = options.tips && !options.silent && !options.watch ? startUsageTipsFetch() : undefined;
47+
3648
const model = await pureGenerate(options, false);
3749

50+
await maybeShowUsageTips?.();
51+
3852
if (options.watch) {
3953
const logsEnabled = !options.silent;
4054

@@ -181,12 +195,18 @@ async function runPlugins(schemaFile: string, model: Model, outputPath: string,
181195

182196
// merge CLI options
183197
if (provider === '@core/typescript') {
184-
if (pluginOptions['lite'] === undefined) {
198+
if (options.lite !== undefined) {
185199
pluginOptions['lite'] = options.lite;
186200
}
187-
if (pluginOptions['liteOnly'] === undefined) {
201+
if (options.liteOnly !== undefined) {
188202
pluginOptions['liteOnly'] = options.liteOnly;
189203
}
204+
if (options.generateModels !== undefined) {
205+
pluginOptions['generateModels'] = options.generateModels;
206+
}
207+
if (options.generateInput !== undefined) {
208+
pluginOptions['generateInput'] = options.generateInput;
209+
}
190210
}
191211

192212
processedPlugins.push({ cliPlugin, pluginOptions });
@@ -196,7 +216,12 @@ async function runPlugins(schemaFile: string, model: Model, outputPath: string,
196216
const defaultPlugins = [
197217
{
198218
plugin: corePlugins['typescript'],
199-
options: { lite: options.lite, liteOnly: options.liteOnly },
219+
options: {
220+
lite: options.lite,
221+
liteOnly: options.liteOnly,
222+
generateModels: options.generateModels,
223+
generateInput: options.generateInput,
224+
},
200225
},
201226
];
202227
defaultPlugins.forEach(({ plugin, options }) => {

packages/cli/src/actions/proxy.ts

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ import { PostgresDialect } from '@zenstackhq/orm/dialects/postgres';
1313
import { SqliteDialect } from '@zenstackhq/orm/dialects/sqlite';
1414
import { RPCApiHandler } from '@zenstackhq/server/api';
1515
import { ZenStackMiddleware } from '@zenstackhq/server/express';
16-
import SQLite from 'better-sqlite3';
16+
import type BetterSqlite3 from 'better-sqlite3';
1717
import colors from 'colors';
1818
import cors from 'cors';
1919
import express from 'express';
2020
import { createJiti } from 'jiti';
21-
import { createPool as createMysqlPool } from 'mysql2';
21+
import type { createPool as MysqlCreatePool } from 'mysql2';
2222
import path from 'node:path';
23-
import { Pool as PgPool } from 'pg';
23+
import type { Pool as PgPoolType } from 'pg';
2424
import { CliError } from '../cli-error';
2525
import { getVersion } from '../utils/version-utils';
2626
import { getOutputPath, getSchemaFile, loadSchemaDocument } from './action-utils';
@@ -67,7 +67,7 @@ export async function run(options: Options) {
6767

6868
const provider = getStringLiteral(dataSource?.fields.find((f) => f.name === 'provider')?.value)!;
6969

70-
const dialect = createDialect(provider, databaseUrl!, outputPath);
70+
const dialect = await createDialect(provider, databaseUrl!, outputPath);
7171

7272
const jiti = createJiti(import.meta.url);
7373

@@ -137,9 +137,17 @@ function redactDatabaseUrl(url: string): string {
137137
}
138138
}
139139

140-
function createDialect(provider: string, databaseUrl: string, outputPath: string) {
140+
async function createDialect(provider: string, databaseUrl: string, outputPath: string) {
141141
switch (provider) {
142142
case 'sqlite': {
143+
let SQLite: typeof BetterSqlite3;
144+
try {
145+
SQLite = (await import('better-sqlite3')).default;
146+
} catch {
147+
throw new CliError(
148+
`Package "better-sqlite3" is required for SQLite support. Please install it with: npm install better-sqlite3`,
149+
);
150+
}
143151
let resolvedUrl = databaseUrl.trim();
144152
if (resolvedUrl.startsWith('file:')) {
145153
const filePath = resolvedUrl.substring('file:'.length);
@@ -152,20 +160,36 @@ function createDialect(provider: string, databaseUrl: string, outputPath: string
152160
database: new SQLite(resolvedUrl),
153161
});
154162
}
155-
case 'postgresql':
163+
case 'postgresql': {
164+
let PgPool: typeof PgPoolType;
165+
try {
166+
PgPool = (await import('pg')).Pool;
167+
} catch {
168+
throw new CliError(
169+
`Package "pg" is required for PostgreSQL support. Please install it with: npm install pg`,
170+
);
171+
}
156172
console.log(colors.gray(`Connecting to PostgreSQL database at: ${redactDatabaseUrl(databaseUrl)}`));
157173
return new PostgresDialect({
158174
pool: new PgPool({
159175
connectionString: databaseUrl,
160176
}),
161177
});
162-
163-
case 'mysql':
178+
}
179+
case 'mysql': {
180+
let createMysqlPool: typeof MysqlCreatePool;
181+
try {
182+
createMysqlPool = (await import('mysql2')).createPool;
183+
} catch {
184+
throw new CliError(
185+
`Package "mysql2" is required for MySQL support. Please install it with: npm install mysql2`,
186+
);
187+
}
164188
console.log(colors.gray(`Connecting to MySQL database at: ${redactDatabaseUrl(databaseUrl)}`));
165189
return new MysqlDialect({
166190
pool: createMysqlPool(databaseUrl),
167191
});
168-
192+
}
169193
default:
170194
throw new CliError(`Unsupported database provider: ${provider}`);
171195
}

0 commit comments

Comments
 (0)