Skip to content

Commit 552b634

Browse files
authored
merge dev to main (v3.4.4) (#2451)
2 parents 51b86db + 75d77de commit 552b634

File tree

36 files changed

+292
-128
lines changed

36 files changed

+292
-128
lines changed

.github/workflows/build-test.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ permissions:
1515

1616
jobs:
1717
build-test:
18-
runs-on: buildjet-8vcpu-ubuntu-2204
18+
runs-on: ubuntu-latest
1919

2020
services:
2121
postgres:
@@ -59,7 +59,7 @@ jobs:
5959
version: 10.12.1
6060

6161
- name: Use Node.js ${{ matrix.node-version }}
62-
uses: buildjet/setup-node@v3
62+
uses: actions/setup-node@v4
6363
with:
6464
node-version: ${{ matrix.node-version }}
6565
cache: 'pnpm'
@@ -76,7 +76,7 @@ jobs:
7676
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
7777
7878
- name: Setup pnpm cache
79-
uses: buildjet/cache@v3
79+
uses: actions/cache@v4
8080
with:
8181
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
8282
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}

.github/workflows/claude-code-review.yml

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,7 @@ on:
1212

1313
jobs:
1414
claude-review:
15-
# Optional: Filter by PR author
16-
# if: |
17-
# github.event.pull_request.user.login == 'external-contributor' ||
18-
# github.event.pull_request.user.login == 'new-developer' ||
19-
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
20-
15+
if: ${{ github.event.pull_request.head.repo.fork == false }}
2116
runs-on: ubuntu-latest
2217
permissions:
2318
contents: read
@@ -33,6 +28,7 @@ jobs:
3328

3429
- name: Run Claude Code Review
3530
id: claude-review
31+
if: ${{ !contains(github.event.pull_request.title, '[WIP]') }}
3632
uses: anthropics/claude-code-action@beta
3733
with:
3834
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
@@ -71,7 +67,3 @@ jobs:
7167
# Optional: Add specific tools for running tests or linting
7268
# allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)"
7369

74-
# Optional: Skip review for certain conditions
75-
# if: |
76-
# !contains(github.event.pull_request.title, '[skip-review]') &&
77-
# !contains(github.event.pull_request.title, '[WIP]')

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "zenstack-v3",
3-
"version": "3.4.3",
3+
"version": "3.4.4",
44
"description": "ZenStack",
55
"packageManager": "pnpm@10.23.0",
66
"type": "module",

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenstackhq/better-auth",
3-
"version": "3.4.3",
3+
"version": "3.4.4",
44
"description": "ZenStack Better Auth Adapter. This adapter is modified from better-auth's Prisma adapter.",
55
"type": "module",
66
"scripts": {

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
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.4.3",
6+
"version": "3.4.4",
77
"type": "module",
88
"author": {
99
"name": "ZenStack Team"

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

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1+
import { invariant } from '@zenstackhq/common-helpers';
12
import { type ZModelServices, loadDocument } from '@zenstackhq/language';
2-
import { type Model, isDataSource } from '@zenstackhq/language/ast';
3-
import { PrismaSchemaGenerator } from '@zenstackhq/sdk';
3+
import { type Model, type Plugin, isDataSource, type LiteralExpr } from '@zenstackhq/language/ast';
4+
import { type CliPlugin, PrismaSchemaGenerator } from '@zenstackhq/sdk';
45
import colors from 'colors';
6+
import { createJiti } from 'jiti';
57
import fs from 'node:fs';
68
import { createRequire } from 'node:module';
79
import path from 'node:path';
8-
import { CliError } from '../cli-error';
10+
import { pathToFileURL } from 'node:url';
911
import terminalLink from 'terminal-link';
1012
import { z } from 'zod';
13+
import { CliError } from '../cli-error';
1114

1215
export function getSchemaFile(file?: string) {
1316
if (file) {
@@ -219,6 +222,89 @@ export async function getZenStackPackages(
219222
return result.filter((p) => !!p);
220223
}
221224

225+
export function getPluginProvider(plugin: Plugin) {
226+
const providerField = plugin.fields.find((f) => f.name === 'provider');
227+
invariant(providerField, `Plugin ${plugin.name} does not have a provider field`);
228+
const provider = (providerField.value as LiteralExpr).value as string;
229+
return provider;
230+
}
231+
232+
export async function loadPluginModule(provider: string, basePath: string) {
233+
if (provider.toLowerCase().endsWith('.zmodel')) {
234+
// provider is a zmodel file, no plugin code module to load
235+
return undefined;
236+
}
237+
238+
let moduleSpec = provider;
239+
if (moduleSpec.startsWith('.')) {
240+
// relative to schema's path
241+
moduleSpec = path.resolve(basePath, moduleSpec);
242+
}
243+
244+
const importAsEsm = async (spec: string) => {
245+
try {
246+
const result = (await import(spec)).default as CliPlugin;
247+
return result;
248+
} catch (err) {
249+
throw new CliError(`Failed to load plugin module from ${spec}: ${(err as Error).message}`);
250+
}
251+
};
252+
253+
const jiti = createJiti(pathToFileURL(basePath).toString());
254+
const importAsTs = async (spec: string) => {
255+
try {
256+
const result = (await jiti.import(spec, { default: true })) as CliPlugin;
257+
return result;
258+
} catch (err) {
259+
throw new CliError(`Failed to load plugin module from ${spec}: ${(err as Error).message}`);
260+
}
261+
};
262+
263+
const esmSuffixes = ['.js', '.mjs'];
264+
const tsSuffixes = ['.ts', '.mts'];
265+
266+
if (fs.existsSync(moduleSpec) && fs.statSync(moduleSpec).isFile()) {
267+
// try provider as ESM file
268+
if (esmSuffixes.some((suffix) => moduleSpec.endsWith(suffix))) {
269+
return await importAsEsm(pathToFileURL(moduleSpec).toString());
270+
}
271+
272+
// try provider as TS file
273+
if (tsSuffixes.some((suffix) => moduleSpec.endsWith(suffix))) {
274+
return await importAsTs(moduleSpec);
275+
}
276+
}
277+
278+
// try ESM index files in provider directory
279+
for (const suffix of esmSuffixes) {
280+
const indexPath = path.join(moduleSpec, `index${suffix}`);
281+
if (fs.existsSync(indexPath)) {
282+
return await importAsEsm(pathToFileURL(indexPath).toString());
283+
}
284+
}
285+
286+
// try TS index files in provider directory
287+
for (const suffix of tsSuffixes) {
288+
const indexPath = path.join(moduleSpec, `index${suffix}`);
289+
if (fs.existsSync(indexPath)) {
290+
return await importAsTs(indexPath);
291+
}
292+
}
293+
294+
// last resort, try to import as esm directly
295+
try {
296+
const mod = await import(moduleSpec);
297+
// plugin may not export a generator, return undefined in that case
298+
return mod.default as CliPlugin | undefined;
299+
} catch (err) {
300+
const errorCode = (err as NodeJS.ErrnoException)?.code;
301+
if (errorCode === 'ERR_MODULE_NOT_FOUND' || errorCode === 'MODULE_NOT_FOUND') {
302+
throw new CliError(`Cannot find plugin module "${provider}". Please make sure the package exists.`);
303+
}
304+
throw new CliError(`Failed to load plugin module "${provider}": ${(err as Error).message}`);
305+
}
306+
}
307+
222308
const FETCH_CLI_MAX_TIME = 1000;
223309
const CLI_CONFIG_ENDPOINT = 'https://zenstack.dev/config/cli-v3.json';
224310

packages/cli/src/actions/check.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { isPlugin, type Model } from '@zenstackhq/language/ast';
12
import colors from 'colors';
2-
import { getSchemaFile, loadSchemaDocument } from './action-utils';
3+
import path from 'node:path';
4+
import { getPluginProvider, getSchemaFile, loadPluginModule, loadSchemaDocument } from './action-utils';
35

46
type Options = {
57
schema?: string;
@@ -12,11 +14,22 @@ export async function run(options: Options) {
1214
const schemaFile = getSchemaFile(options.schema);
1315

1416
try {
15-
await loadSchemaDocument(schemaFile);
17+
const model = await loadSchemaDocument(schemaFile);
18+
await checkPluginResolution(schemaFile, model);
1619
console.log(colors.green('✓ Schema validation completed successfully.'));
1720
} catch (error) {
1821
console.error(colors.red('✗ Schema validation failed.'));
1922
// Re-throw to maintain CLI exit code behavior
2023
throw error;
2124
}
2225
}
26+
27+
async function checkPluginResolution(schemaFile: string, model: Model) {
28+
const plugins = model.declarations.filter(isPlugin);
29+
for (const plugin of plugins) {
30+
const provider = getPluginProvider(plugin);
31+
if (!provider.startsWith('@core/')) {
32+
await loadPluginModule(provider, path.dirname(schemaFile));
33+
}
34+
}
35+
}

packages/cli/src/actions/generate.ts

Lines changed: 4 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,21 @@
11
import { invariant, singleDebounce } from '@zenstackhq/common-helpers';
22
import { ZModelLanguageMetaData } from '@zenstackhq/language';
3-
import { isPlugin, LiteralExpr, Plugin, type AbstractDeclaration, type Model } from '@zenstackhq/language/ast';
3+
import { isPlugin, type AbstractDeclaration, type Model } from '@zenstackhq/language/ast';
44
import { getLiteral, getLiteralArray } from '@zenstackhq/language/utils';
55
import { type CliPlugin } from '@zenstackhq/sdk';
66
import { watch } from 'chokidar';
77
import colors from 'colors';
8-
import { createJiti } from 'jiti';
9-
import fs from 'node:fs';
108
import path from 'node:path';
11-
import { pathToFileURL } from 'node:url';
129
import ora, { type Ora } from 'ora';
1310
import semver from 'semver';
1411
import { CliError } from '../cli-error';
1512
import * as corePlugins from '../plugins';
1613
import {
1714
getOutputPath,
15+
getPluginProvider,
1816
getSchemaFile,
1917
getZenStackPackages,
18+
loadPluginModule,
2019
loadSchemaDocument,
2120
startUsageTipsFetch,
2221
} from './action-utils';
@@ -258,14 +257,7 @@ async function runPlugins(schemaFile: string, model: Model, outputPath: string,
258257
}
259258
}
260259

261-
function getPluginProvider(plugin: Plugin) {
262-
const providerField = plugin.fields.find((f) => f.name === 'provider');
263-
invariant(providerField, `Plugin ${plugin.name} does not have a provider field`);
264-
const provider = (providerField.value as LiteralExpr).value as string;
265-
return provider;
266-
}
267-
268-
function getPluginOptions(plugin: Plugin): Record<string, unknown> {
260+
function getPluginOptions(plugin: Parameters<typeof getPluginProvider>[0]): Record<string, unknown> {
269261
const result: Record<string, unknown> = {};
270262
for (const field of plugin.fields) {
271263
if (field.name === 'provider') {
@@ -281,72 +273,6 @@ function getPluginOptions(plugin: Plugin): Record<string, unknown> {
281273
return result;
282274
}
283275

284-
async function loadPluginModule(provider: string, basePath: string) {
285-
let moduleSpec = provider;
286-
if (moduleSpec.startsWith('.')) {
287-
// relative to schema's path
288-
moduleSpec = path.resolve(basePath, moduleSpec);
289-
}
290-
291-
const importAsEsm = async (spec: string) => {
292-
try {
293-
const result = (await import(spec)).default as CliPlugin;
294-
return result;
295-
} catch (err) {
296-
throw new CliError(`Failed to load plugin module from ${spec}: ${(err as Error).message}`);
297-
}
298-
};
299-
300-
const jiti = createJiti(pathToFileURL(basePath).toString());
301-
const importAsTs = async (spec: string) => {
302-
try {
303-
const result = (await jiti.import(spec, { default: true })) as CliPlugin;
304-
return result;
305-
} catch (err) {
306-
throw new CliError(`Failed to load plugin module from ${spec}: ${(err as Error).message}`);
307-
}
308-
};
309-
310-
const esmSuffixes = ['.js', '.mjs'];
311-
const tsSuffixes = ['.ts', '.mts'];
312-
313-
if (fs.existsSync(moduleSpec) && fs.statSync(moduleSpec).isFile()) {
314-
// try provider as ESM file
315-
if (esmSuffixes.some((suffix) => moduleSpec.endsWith(suffix))) {
316-
return await importAsEsm(pathToFileURL(moduleSpec).toString());
317-
}
318-
319-
// try provider as TS file
320-
if (tsSuffixes.some((suffix) => moduleSpec.endsWith(suffix))) {
321-
return await importAsTs(moduleSpec);
322-
}
323-
}
324-
325-
// try ESM index files in provider directory
326-
for (const suffix of esmSuffixes) {
327-
const indexPath = path.join(moduleSpec, `index${suffix}`);
328-
if (fs.existsSync(indexPath)) {
329-
return await importAsEsm(pathToFileURL(indexPath).toString());
330-
}
331-
}
332-
333-
// try TS index files in provider directory
334-
for (const suffix of tsSuffixes) {
335-
const indexPath = path.join(moduleSpec, `index${suffix}`);
336-
if (fs.existsSync(indexPath)) {
337-
return await importAsTs(indexPath);
338-
}
339-
}
340-
341-
// last resort, try to import as esm directly
342-
try {
343-
return (await import(moduleSpec)).default as CliPlugin;
344-
} catch {
345-
// plugin may not export a generator so we simply ignore the error here
346-
return undefined;
347-
}
348-
}
349-
350276
async function checkForMismatchedPackages(projectPath: string) {
351277
const packages = await getZenStackPackages(projectPath);
352278
if (!packages.length) {

packages/cli/src/actions/proxy.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,18 +75,18 @@ export async function run(options: Options) {
7575

7676
const schemaModule = (await jiti.import(path.join(outputPath, 'schema'))) as any;
7777

78-
// Build omit configuration for computed fields
78+
// Build omit configuration for computed fields and Unsupported fields.
7979
const schema = schemaModule.schema as SchemaDef;
8080
const omit: Record<string, Record<string, boolean>> = {};
8181
for (const [modelName, modelDef] of Object.entries(schema.models)) {
82-
const computedFields: Record<string, boolean> = {};
82+
const omitFields: Record<string, boolean> = {};
8383
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
84-
if (fieldDef.computed === true) {
85-
computedFields[fieldName] = true;
84+
if (fieldDef.computed === true || fieldDef.type === 'Unsupported') {
85+
omitFields[fieldName] = true;
8686
}
8787
}
88-
if (Object.keys(computedFields).length > 0) {
89-
omit[modelName] = computedFields;
88+
if (Object.keys(omitFields).length > 0) {
89+
omit[modelName] = omitFields;
9090
}
9191
}
9292

0 commit comments

Comments
 (0)