Skip to content

Commit b0996b4

Browse files
authored
Merge pull request #580 from zenstackhq/dev
merge dev to main (v3.2.0)
2 parents 17d4ff2 + c4ee20a commit b0996b4

File tree

155 files changed

+8798
-1044
lines changed

Some content is hidden

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

155 files changed

+8798
-1044
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
I want to thank you first for considering contributing to ZenStack 🙏🏻. It's people like you who make ZenStack a better toolkit that benefits more developers!
44

5-
Before you start working on anything major, please make sure to open an issue or discuss with us in our [Discord](https://discord.gg/Ykhr738dUe) first. This will help ensure your work aligns with the project's goals and avoid duplication of effort.
5+
Before you start working on anything major, please make sure to create a topic in the [feature-work](https://discord.com/channels/1035538056146595961/1458658287015952498) discord channel (preferred) or create a GitHub issue to discuss it first. This will help ensure your work aligns with the project's goals and avoid duplication of effort.
66

77
## Prerequisites
88

TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@
7777
- [x] JSDoc for CRUD methods
7878
- [x] Cache validation schemas
7979
- [x] Compound ID
80-
- [ ] Cross field comparison
8180
- [x] Many-to-many relation
8281
- [x] Self relation
8382
- [ ] Empty AND/OR/NOT behavior
@@ -101,6 +100,7 @@
101100
- [x] Validation
102101
- [ ] Access Policy
103102
- [ ] Short-circuit pre-create check for scalar-field only policies
103+
- [x] Field-level policies
104104
- [x] Inject "on conflict do update"
105105
- [x] `check` function
106106
- [ ] Custom functions

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.1.1",
3+
"version": "3.2.0",
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.1.1",
3+
"version": "3.2.0",
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: 9 additions & 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.1.1",
6+
"version": "3.2.0",
77
"type": "module",
88
"author": {
99
"name": "ZenStack Team"
@@ -21,6 +21,10 @@
2121
"zen": "bin/cli",
2222
"zenstack": "bin/cli"
2323
},
24+
"files": [
25+
"dist",
26+
"bin"
27+
],
2428
"scripts": {
2529
"build": "tsc --noEmit && tsup-node",
2630
"watch": "tsup-node --watch",
@@ -35,6 +39,7 @@
3539
"@zenstackhq/common-helpers": "workspace:*",
3640
"@zenstackhq/language": "workspace:*",
3741
"@zenstackhq/sdk": "workspace:*",
42+
"chokidar": "^5.0.0",
3843
"colors": "1.4.0",
3944
"commander": "^8.3.0",
4045
"execa": "^9.6.0",
@@ -58,5 +63,8 @@
5863
"@zenstackhq/vitest-config": "workspace:*",
5964
"better-sqlite3": "catalog:",
6065
"tmp": "catalog:"
66+
},
67+
"engines": {
68+
"node": ">=20"
6169
}
6270
}

packages/cli/src/actions/generate.ts

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import { invariant } from '@zenstackhq/common-helpers';
2-
import { isPlugin, LiteralExpr, Plugin, type Model } from '@zenstackhq/language/ast';
1+
import { invariant, singleDebounce } from '@zenstackhq/common-helpers';
2+
import { ZModelLanguageMetaData } from '@zenstackhq/language';
3+
import { type AbstractDeclaration, isPlugin, LiteralExpr, Plugin, type Model } from '@zenstackhq/language/ast';
34
import { getLiteral, getLiteralArray } from '@zenstackhq/language/utils';
45
import { type CliPlugin } from '@zenstackhq/sdk';
56
import colors from 'colors';
67
import { createJiti } from 'jiti';
78
import fs from 'node:fs';
89
import path from 'node:path';
910
import { pathToFileURL } from 'node:url';
11+
import { watch } from 'chokidar';
1012
import ora, { type Ora } from 'ora';
1113
import { CliError } from '../cli-error';
1214
import * as corePlugins from '../plugins';
@@ -16,6 +18,7 @@ type Options = {
1618
schema?: string;
1719
output?: string;
1820
silent: boolean;
21+
watch: boolean;
1922
lite: boolean;
2023
liteOnly: boolean;
2124
};
@@ -24,6 +27,96 @@ type Options = {
2427
* CLI action for generating code from schema
2528
*/
2629
export async function run(options: Options) {
30+
const model = await pureGenerate(options, false);
31+
32+
if (options.watch) {
33+
const logsEnabled = !options.silent;
34+
35+
if (logsEnabled) {
36+
console.log(colors.green(`\nEnabled watch mode!`));
37+
}
38+
39+
const schemaExtensions = ZModelLanguageMetaData.fileExtensions;
40+
41+
// Get real models file path (cuz its merged into single document -> we need use cst nodes)
42+
const getRootModelWatchPaths = (model: Model) => new Set<string>(
43+
(
44+
model.declarations.filter(
45+
(v) =>
46+
v.$cstNode?.parent?.element.$type === 'Model' &&
47+
!!v.$cstNode.parent.element.$document?.uri?.fsPath,
48+
) as AbstractDeclaration[]
49+
).map((v) => v.$cstNode!.parent!.element.$document!.uri!.fsPath),
50+
);
51+
52+
const watchedPaths = getRootModelWatchPaths(model);
53+
54+
if (logsEnabled) {
55+
const logPaths = [...watchedPaths].map((at) => `- ${at}`).join('\n');
56+
console.log(`Watched file paths:\n${logPaths}`);
57+
}
58+
59+
const watcher = watch([...watchedPaths], {
60+
alwaysStat: false,
61+
ignoreInitial: true,
62+
ignorePermissionErrors: true,
63+
ignored: (at) => !schemaExtensions.some((ext) => at.endsWith(ext)),
64+
});
65+
66+
// prevent save multiple files and run multiple times
67+
const reGenerateSchema = singleDebounce(async () => {
68+
if (logsEnabled) {
69+
console.log('Got changes, run generation!');
70+
}
71+
72+
try {
73+
const newModel = await pureGenerate(options, true);
74+
const allModelsPaths = getRootModelWatchPaths(newModel);
75+
const newModelPaths = [...allModelsPaths].filter((at) => !watchedPaths.has(at));
76+
const removeModelPaths = [...watchedPaths].filter((at) => !allModelsPaths.has(at));
77+
78+
if (newModelPaths.length) {
79+
if (logsEnabled) {
80+
const logPaths = newModelPaths.map((at) => `- ${at}`).join('\n');
81+
console.log(`Added file(s) to watch:\n${logPaths}`);
82+
}
83+
84+
newModelPaths.forEach((at) => watchedPaths.add(at));
85+
watcher.add(newModelPaths);
86+
}
87+
88+
if (removeModelPaths.length) {
89+
if (logsEnabled) {
90+
const logPaths = removeModelPaths.map((at) => `- ${at}`).join('\n');
91+
console.log(`Removed file(s) from watch:\n${logPaths}`);
92+
}
93+
94+
removeModelPaths.forEach((at) => watchedPaths.delete(at));
95+
watcher.unwatch(removeModelPaths);
96+
}
97+
} catch (e) {
98+
console.error(e);
99+
}
100+
}, 500, true);
101+
102+
watcher.on('unlink', (pathAt) => {
103+
if (logsEnabled) {
104+
console.log(`Removed file from watch: ${pathAt}`);
105+
}
106+
107+
watchedPaths.delete(pathAt);
108+
watcher.unwatch(pathAt);
109+
110+
reGenerateSchema();
111+
});
112+
113+
watcher.on('change', () => {
114+
reGenerateSchema();
115+
});
116+
}
117+
}
118+
119+
async function pureGenerate(options: Options, fromWatch: boolean) {
27120
const start = Date.now();
28121

29122
const schemaFile = getSchemaFile(options.schema);
@@ -35,7 +128,9 @@ export async function run(options: Options) {
35128

36129
if (!options.silent) {
37130
console.log(colors.green(`Generation completed successfully in ${Date.now() - start}ms.\n`));
38-
console.log(`You can now create a ZenStack client with it.
131+
132+
if (!fromWatch) {
133+
console.log(`You can now create a ZenStack client with it.
39134
40135
\`\`\`ts
41136
import { ZenStackClient } from '@zenstackhq/orm';
@@ -47,7 +142,10 @@ const client = new ZenStackClient(schema, {
47142
\`\`\`
48143
49144
Check documentation: https://zenstack.dev/docs/`);
145+
}
50146
}
147+
148+
return model;
51149
}
52150

53151
function getOutputPath(options: Options, schemaFile: string) {

packages/cli/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ function createProgram() {
6868
.addOption(schemaOption)
6969
.addOption(noVersionCheckOption)
7070
.addOption(new Option('-o, --output <path>', 'default output directory for code generation'))
71+
.addOption(new Option('-w, --watch', 'enable watch mode').default(false))
7172
.addOption(new Option('--lite', 'also generate a lite version of schema without attributes').default(false))
7273
.addOption(new Option('--lite-only', 'only generate lite version of schema without attributes').default(false))
7374
.addOption(new Option('--silent', 'suppress all output except errors').default(false))
@@ -220,6 +221,11 @@ async function main() {
220221
}
221222
}
222223

224+
if (program.args.includes('generate') && (program.args.includes('-w') || program.args.includes('--watch'))) {
225+
// A "hack" way to prevent the process from terminating because we don't want to stop it.
226+
return;
227+
}
228+
223229
if (telemetry.isTracking) {
224230
// give telemetry a chance to send events before exit
225231
setTimeout(() => {

packages/cli/test/ts-schema-gen.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,30 @@ model Post {
184184
});
185185
});
186186

187+
it('generates correct procedures with array params and returns', async () => {
188+
const { schema } = await generateTsSchema(`
189+
model User {
190+
id Int @id
191+
}
192+
193+
procedure findByIds(ids: Int[]): User[]
194+
procedure getIds(): Int[]
195+
`);
196+
197+
expect(schema.procedures).toMatchObject({
198+
findByIds: {
199+
params: { ids: { name: 'ids', type: 'Int', array: true } },
200+
returnType: 'User',
201+
returnArray: true,
202+
},
203+
getIds: {
204+
params: {},
205+
returnType: 'Int',
206+
returnArray: true,
207+
},
208+
});
209+
});
210+
187211
it('merges fields and attributes from mixins', async () => {
188212
const { schema } = await generateTsSchema(`
189213
type Timestamped {

packages/clients/client-helpers/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenstackhq/client-helpers",
3-
"version": "3.1.1",
3+
"version": "3.2.0",
44
"description": "Helpers for implementing clients that consume ZenStack's CRUD service",
55
"type": "module",
66
"scripts": {
@@ -13,6 +13,9 @@
1313
},
1414
"author": "ZenStack Team",
1515
"license": "MIT",
16+
"files": [
17+
"dist"
18+
],
1619
"exports": {
1720
".": {
1821
"types": "./dist/index.d.ts",

packages/clients/tanstack-query/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenstackhq/tanstack-query",
3-
"version": "3.1.1",
3+
"version": "3.2.0",
44
"description": "TanStack Query Client for consuming ZenStack v3's CRUD service",
55
"type": "module",
66
"scripts": {

0 commit comments

Comments
 (0)