Skip to content

Commit cc24964

Browse files
authored
merge dev to main (v3.7.0) (#2667)
2 parents 71c97a8 + ed01275 commit cc24964

153 files changed

Lines changed: 13207 additions & 1316 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.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "zenstack-v3",
33
"displayName": "ZenStack",
44
"description": "ZenStack",
5-
"version": "3.6.4",
5+
"version": "3.7.0",
66
"type": "module",
77
"author": {
88
"name": "ZenStack Team",
@@ -63,7 +63,8 @@
6363
"overrides": {
6464
"cookie@<0.7.0": ">=0.7.0",
6565
"lodash-es@>=4.0.0 <=4.17.22": ">=4.17.23",
66-
"lodash@>=4.0.0 <=4.17.22": ">=4.17.23"
66+
"lodash@>=4.0.0 <=4.17.22": ">=4.17.23",
67+
"@better-auth/core": "1.4.19"
6768
}
6869
},
6970
"funding": "https://github.com/sponsors/zenstackhq"

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@zenstackhq/better-auth",
33
"displayName": "ZenStack Better Auth Adapter",
44
"description": "ZenStack Better Auth Adapter. This adapter is modified from better-auth's Prisma adapter.",
5-
"version": "3.6.4",
5+
"version": "3.7.0",
66
"type": "module",
77
"author": {
88
"name": "ZenStack Team",

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import type { BetterAuthOptions } from '@better-auth/core';
2-
import type { DBAdapter, Where } from '@better-auth/core/db/adapter';
1+
import type { BetterAuthOptions, Where } from 'better-auth';
32
import { BetterAuthError } from '@better-auth/core/error';
43
import type { ClientContract, ModelOperations, UpdateInput } from '@zenstackhq/orm';
54
import type { GetModels, SchemaDef } from '@zenstackhq/orm/schema';
@@ -187,7 +186,9 @@ export const zenstackAdapter = <Schema extends SchemaDef>(db: ClientContract<Sch
187186
options: config,
188187

189188
createSchema: async ({ file, tables }) => {
190-
const generateSchema = (await import('./schema-generator')).generateSchema;
189+
// Self-import via package subpath (not a relative './schema-generator') so the
190+
// bundler treats it as external and keeps it lazy in the CJS output — see tsdown.config.ts.
191+
const generateSchema = (await import('@zenstackhq/better-auth/schema-generator')).generateSchema;
191192
return generateSchema(file, tables, config, options);
192193
},
193194
};
@@ -213,7 +214,7 @@ export const zenstackAdapter = <Schema extends SchemaDef>(db: ClientContract<Sch
213214
};
214215

215216
const adapter = createAdapterFactory(adapterOptions);
216-
return (options: BetterAuthOptions): DBAdapter<BetterAuthOptions> => {
217+
return (options: BetterAuthOptions) => {
217218
lazyOptions = options;
218219
return adapter(options);
219220
};

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
"extends": "@zenstackhq/typescript-config/base.json",
33
"compilerOptions": {
44
"rootDir": ".",
5-
"noPropertyAccessFromIndexSignature": false
5+
"noPropertyAccessFromIndexSignature": false,
6+
"types": ["node"],
7+
"paths": {
8+
"@zenstackhq/better-auth/schema-generator": ["./src/schema-generator.ts"]
9+
}
610
},
711
"include": ["src/**/*"]
812
}
Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
11
import { createConfig } from '@zenstackhq/tsdown-config';
22

3-
export default createConfig({ entry: { index: 'src/index.ts', 'schema-generator': 'src/schema-generator.ts' } });
3+
// `index` and `schema-generator` are built as two separate tsdown invocations so that
4+
// the lazy `await import('@zenstackhq/better-auth/schema-generator')` in the adapter
5+
// stays lazy in the CJS output. When both entries live in a single build, Rolldown
6+
// treats them as siblings and injects a top-level `require('./schema-generator.cjs')`
7+
// into `index.cjs`, which eagerly pulls in `@zenstackhq/language` (Langium) at adapter
8+
// load time. Splitting the builds hides that relationship; `neverBundle` then keeps
9+
// the dynamic import as a package-name reference that Node resolves at first call.
10+
export default [
11+
createConfig({
12+
entry: { index: 'src/index.ts' },
13+
deps: { neverBundle: ['@zenstackhq/better-auth/schema-generator'] },
14+
}),
15+
createConfig({
16+
entry: { 'schema-generator': 'src/schema-generator.ts' },
17+
}),
18+
];

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@zenstackhq/cli",
33
"displayName": "ZenStack CLI",
44
"description": "FullStack database toolkit with built-in access control and automatic API generation.",
5-
"version": "3.6.4",
5+
"version": "3.7.0",
66
"type": "module",
77
"author": {
88
"name": "ZenStack Team",

packages/cli/src/actions/db.ts

Lines changed: 71 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { formatDocument, ZModelCodeGenerator } from '@zenstackhq/language';
2-
import { DataModel, Enum, type Model } from '@zenstackhq/language/ast';
2+
import { DataModel, Enum, isDataField, type DataField, type Model } from '@zenstackhq/language/ast';
33
import colors from 'colors';
44
import fs from 'node:fs';
55
import path from 'node:path';
@@ -14,7 +14,7 @@ import {
1414
} from './action-utils';
1515
import { consolidateEnums, syncEnums, syncRelation, syncTable, type Relation } from './pull';
1616
import { providers as pullProviders } from './pull/provider';
17-
import { getDatasource, getDbName, getRelationFieldsKey, getRelationFkName, isDatabaseManagedAttribute } from './pull/utils';
17+
import { getDatasource, getDbName, getRelationFieldsKey, getRelationFkName, getRelationName, isDatabaseManagedAttribute } from './pull/utils';
1818
import type { DataSourceProviderType } from '@zenstackhq/schema';
1919
import { CliError } from '../cli-error';
2020

@@ -35,6 +35,25 @@ export type PullOptions = {
3535
indent: number;
3636
};
3737

38+
function hasRelationFieldsArg(field: DataField) {
39+
const relationAttr = field.attributes.find((a) => a.decl.ref?.name === '@relation');
40+
return !!relationAttr?.args.some((a) => a.name === 'fields');
41+
}
42+
43+
function getReferencedModelName(field: DataField) {
44+
return field.type.reference?.ref ? getDbName(field.type.reference.ref) : undefined;
45+
}
46+
47+
function matchesRelationNameFallback(field: DataField, relationName: string, candidate: DataField) {
48+
const referencedModelName = getReferencedModelName(field);
49+
return (
50+
!!referencedModelName &&
51+
getRelationName(candidate) === relationName &&
52+
hasRelationFieldsArg(candidate) === hasRelationFieldsArg(field) &&
53+
getReferencedModelName(candidate) === referencedModelName
54+
);
55+
}
56+
3857
/**
3958
* CLI action for db related commands
4059
*/
@@ -283,46 +302,52 @@ async function runPull(options: PullOptions) {
283302
}
284303

285304
newDataModel.fields.forEach((f) => {
286-
// Prioritized matching: exact db name > relation fields key > relation FK name > type reference
305+
// Prioritized matching: exact db name > relation fields key > relation FK name > relation name > type reference
287306
let originalFields = originalDataModel.fields.filter((d) => getDbName(d) === getDbName(f));
288307

289-
// If this is a back-reference relation field (has @relation but no `fields` arg), silently skip
290-
const isRelationField =
291-
f.$type === 'DataField' && !!(f as any).attributes?.some((a: any) => a?.decl?.ref?.name === '@relation');
292-
if (originalFields.length === 0 && isRelationField && !getRelationFieldsKey(f as any)) {
293-
return;
294-
}
295-
296308
if (originalFields.length === 0) {
297309
// Try matching by relation fields key (the `fields` attribute in @relation)
298310
// This matches relation fields by their FK field references
299-
const newFieldsKey = getRelationFieldsKey(f as any);
311+
const newFieldsKey = isDataField(f) ? getRelationFieldsKey(f) : undefined;
300312
if (newFieldsKey) {
301313
originalFields = originalDataModel.fields.filter(
302-
(d) => getRelationFieldsKey(d as any) === newFieldsKey,
314+
(d) => isDataField(d) && getRelationFieldsKey(d) === newFieldsKey,
303315
);
304316
}
305317
}
306318

307319
if (originalFields.length === 0) {
308320
// Try matching by relation FK name (the `map` attribute in @relation)
309-
originalFields = originalDataModel.fields.filter(
310-
(d) =>
311-
getRelationFkName(d as any) === getRelationFkName(f as any) &&
312-
!!getRelationFkName(d as any) &&
313-
!!getRelationFkName(f as any),
314-
);
321+
const newFkName = isDataField(f) ? getRelationFkName(f) : undefined;
322+
if (newFkName) {
323+
originalFields = originalDataModel.fields.filter(
324+
(d) => isDataField(d) && getRelationFkName(d) === newFkName,
325+
);
326+
}
327+
}
328+
329+
if (originalFields.length === 0) {
330+
// Try matching by relation name (the `name` arg in @relation)
331+
// This is essential for back-reference fields that only have a relation name
332+
const newRelName = isDataField(f) ? getRelationName(f) : undefined;
333+
if (newRelName) {
334+
originalFields = originalDataModel.fields.filter(
335+
(d) =>
336+
isDataField(d) &&
337+
isDataField(f) &&
338+
matchesRelationNameFallback(f, newRelName, d),
339+
);
340+
}
315341
}
316342

317343
if (originalFields.length === 0) {
318344
// Try matching by type reference
319345
// We need this because for relations that don't have @relation, we can only check if the original exists by the field type.
320346
// Yes, in this case it can potentially result in multiple original fields, but we only want to ensure that at least one relation exists.
321-
// In the future, we might implement some logic to detect how many of these types of relations we need and add/remove fields based on this.
322347
originalFields = originalDataModel.fields.filter(
323348
(d) =>
324-
f.$type === 'DataField' &&
325-
d.$type === 'DataField' &&
349+
isDataField(f) &&
350+
isDataField(d) &&
326351
f.type.reference?.ref &&
327352
d.type.reference?.ref &&
328353
getDbName(f.type.reference.ref) === getDbName(d.type.reference.ref),
@@ -332,7 +357,7 @@ async function runPull(options: PullOptions) {
332357
if (originalFields.length > 1) {
333358
// If this is a back-reference relation field (no `fields` attribute),
334359
// silently skip when there are multiple potential matches
335-
const isBackReferenceField = !getRelationFieldsKey(f as any);
360+
const isBackReferenceField = isDataField(f) && !getRelationFieldsKey(f);
336361
if (!isBackReferenceField) {
337362
console.warn(
338363
colors.yellow(
@@ -499,31 +524,43 @@ async function runPull(options: PullOptions) {
499524
});
500525
originalDataModel.fields
501526
.filter((f) => {
502-
// Prioritized matching: exact db name > relation fields key > relation FK name > type reference
527+
// Prioritized matching: exact db name > relation fields key > relation FK name > relation name > type reference
503528
const matchByDbName = newDataModel.fields.find((d) => getDbName(d) === getDbName(f));
504529
if (matchByDbName) return false;
505530

506531
// Try matching by relation fields key (the `fields` attribute in @relation)
507-
const originalFieldsKey = getRelationFieldsKey(f as any);
532+
const originalFieldsKey = isDataField(f) ? getRelationFieldsKey(f) : undefined;
508533
if (originalFieldsKey) {
509534
const matchByFieldsKey = newDataModel.fields.find(
510-
(d) => getRelationFieldsKey(d as any) === originalFieldsKey,
535+
(d) => isDataField(d) && getRelationFieldsKey(d) === originalFieldsKey,
511536
);
512537
if (matchByFieldsKey) return false;
513538
}
514539

515-
const matchByFkName = newDataModel.fields.find(
516-
(d) =>
517-
getRelationFkName(d as any) === getRelationFkName(f as any) &&
518-
!!getRelationFkName(d as any) &&
519-
!!getRelationFkName(f as any),
520-
);
521-
if (matchByFkName) return false;
540+
const originalFkName = isDataField(f) ? getRelationFkName(f) : undefined;
541+
if (originalFkName) {
542+
const matchByFkName = newDataModel.fields.find(
543+
(d) => isDataField(d) && getRelationFkName(d) === originalFkName,
544+
);
545+
if (matchByFkName) return false;
546+
}
547+
548+
// Try matching by relation name (for named back-reference fields)
549+
const originalRelName = isDataField(f) ? getRelationName(f) : undefined;
550+
if (originalRelName) {
551+
const matchByRelName = newDataModel.fields.find(
552+
(d) =>
553+
isDataField(d) &&
554+
isDataField(f) &&
555+
matchesRelationNameFallback(f, originalRelName, d),
556+
);
557+
if (matchByRelName) return false;
558+
}
522559

523560
const matchByTypeRef = newDataModel.fields.find(
524561
(d) =>
525-
f.$type === 'DataField' &&
526-
d.$type === 'DataField' &&
562+
isDataField(f) &&
563+
isDataField(d) &&
527564
f.type.reference?.ref &&
528565
d.type.reference?.ref &&
529566
getDbName(f.type.reference.ref) === getDbName(d.type.reference.ref),

packages/cli/src/actions/proxy.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ZenStackClient, type ClientContract } from '@zenstackhq/orm';
1111
import { MysqlDialect } from '@zenstackhq/orm/dialects/mysql';
1212
import { PostgresDialect } from '@zenstackhq/orm/dialects/postgres';
1313
import { SqliteDialect } from '@zenstackhq/orm/dialects/sqlite';
14+
import type { SchemaDef } from '@zenstackhq/orm/schema';
1415
import { RPCApiHandler } from '@zenstackhq/server/api';
1516
import { ZenStackMiddleware } from '@zenstackhq/server/express';
1617
import type BetterSqlite3 from 'better-sqlite3';
@@ -24,7 +25,6 @@ import type { Pool as PgPoolType } from 'pg';
2425
import { CliError } from '../cli-error';
2526
import { getVersion } from '../utils/version-utils';
2627
import { getOutputPath, getSchemaFile, loadSchemaDocument } from './action-utils';
27-
import type { SchemaDef } from '@zenstackhq/orm/schema';
2828

2929
type Options = {
3030
output?: string;
@@ -198,7 +198,7 @@ async function createDialect(provider: string, databaseUrl: string, outputPath:
198198
}
199199
}
200200

201-
export function createProxyApp(client: ClientContract<any, any>, schema: any): express.Application {
201+
export function createProxyApp(client: ClientContract<SchemaDef>, schema: SchemaDef): express.Application {
202202
const app = express();
203203
app.use(cors());
204204
app.use(express.json({ limit: '5mb' }));
@@ -219,7 +219,7 @@ export function createProxyApp(client: ClientContract<any, any>, schema: any): e
219219
return app;
220220
}
221221

222-
function startServer(client: ClientContract<any, any>, schema: any, options: Options) {
222+
function startServer(client: ClientContract<SchemaDef>, schema: any, options: Options) {
223223
const app = createProxyApp(client, schema);
224224

225225
const server = app.listen(options.port, () => {

packages/cli/src/actions/pull/utils.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
type StringLiteral,
1515
} from '@zenstackhq/language/ast';
1616
import type { AstFactory, ExpressionBuilder } from '@zenstackhq/language/factory';
17-
import { getLiteralArray, getStringLiteral } from '@zenstackhq/language/utils';
17+
import { getAttributeArgLiteral, getLiteralArray, getStringLiteral } from '@zenstackhq/language/utils';
1818
import type { DataSourceProviderType } from '@zenstackhq/schema';
1919
import type { Reference } from 'langium';
2020
import { CliError } from '../../cli-error';
@@ -122,6 +122,19 @@ export function getRelationFkName(decl: DataField): string | undefined {
122122
return schemaAttrValue?.value;
123123
}
124124

125+
/**
126+
* Gets the relation name from the @relation attribute's `name` argument.
127+
* e.g., @relation('myRelation', fields: [...], references: [...]) -> "myRelation"
128+
* e.g., @relation(name: 'myRelation', fields: [...], references: [...]) -> "myRelation"
129+
* e.g., @relation(fields: [...], references: [...]) -> undefined
130+
* e.g., @relation('backRef') -> "backRef"
131+
*/
132+
export function getRelationName(decl: DataField): string | undefined {
133+
const relationAttr = decl?.attributes?.find((a) => a.decl?.ref?.name === '@relation');
134+
if (!relationAttr) return undefined;
135+
return getAttributeArgLiteral(relationAttr, 'name');
136+
}
137+
125138
/**
126139
* Gets the FK field names from the @relation attribute's `fields` argument.
127140
* Returns a sorted, comma-separated string of field names for comparison.

0 commit comments

Comments
 (0)