From 639057063224b92ab5f5c16009e59683a23579b4 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:27:23 -0800 Subject: [PATCH 1/2] feat: better-auth adapter --- .../better-auth/eslint.config.js | 9 + .../auth-adapters/better-auth/package.json | 54 ++ .../auth-adapters/better-auth/src/adapter.ts | 241 ++++++++ .../auth-adapters/better-auth/src/index.ts | 1 + .../better-auth/src/schema-generator.ts | 546 ++++++++++++++++++ .../auth-adapters/better-auth/tsconfig.json | 8 + .../auth-adapters/better-auth/tsup.config.ts | 13 + .../better-auth/vitest.config.ts | 4 + packages/cli/src/actions/action-utils.ts | 6 +- .../language/src/zmodel-code-generator.ts | 11 +- packages/orm/src/client/crud-types.ts | 17 +- pnpm-lock.yaml | 364 ++++++++++++ 12 files changed, 1259 insertions(+), 15 deletions(-) create mode 100644 packages/auth-adapters/better-auth/eslint.config.js create mode 100644 packages/auth-adapters/better-auth/package.json create mode 100644 packages/auth-adapters/better-auth/src/adapter.ts create mode 100644 packages/auth-adapters/better-auth/src/index.ts create mode 100644 packages/auth-adapters/better-auth/src/schema-generator.ts create mode 100644 packages/auth-adapters/better-auth/tsconfig.json create mode 100644 packages/auth-adapters/better-auth/tsup.config.ts create mode 100644 packages/auth-adapters/better-auth/vitest.config.ts diff --git a/packages/auth-adapters/better-auth/eslint.config.js b/packages/auth-adapters/better-auth/eslint.config.js new file mode 100644 index 00000000..f04dc9fe --- /dev/null +++ b/packages/auth-adapters/better-auth/eslint.config.js @@ -0,0 +1,9 @@ +import config from '@zenstackhq/eslint-config/base.js'; +import tseslint from 'typescript-eslint'; + +/** @type {import("eslint").Linter.Config} */ +export default tseslint.config(config, { + rules: { + '@typescript-eslint/no-unused-expressions': 'off', + }, +}); diff --git a/packages/auth-adapters/better-auth/package.json b/packages/auth-adapters/better-auth/package.json new file mode 100644 index 00000000..acf41118 --- /dev/null +++ b/packages/auth-adapters/better-auth/package.json @@ -0,0 +1,54 @@ +{ + "name": "@zenstackhq/better-auth", + "version": "3.0.0-beta.23", + "description": "ZenStack Better Auth Adapter. This adapter is modified from better-auth's Prisma adapter.", + "type": "module", + "scripts": { + "build": "tsc --noEmit && tsup-node", + "watch": "tsup-node --watch", + "lint": "eslint src --ext ts", + "pack": "pnpm pack" + }, + "keywords": [ + "better-auth", + "auth" + ], + "author": "ZenStack Team", + "license": "MIT", + "files": [ + "dist" + ], + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./package.json": { + "import": "./package.json", + "require": "./package.json" + } + }, + "dependencies": { + "@zenstackhq/orm": "workspace:*", + "@zenstackhq/language": "workspace:*", + "@zenstackhq/common-helpers": "workspace:*", + "ts-pattern": "catalog:" + }, + "peerDependencies": { + "@better-auth/core": "^1.0.0", + "better-auth": "^1.0.0" + }, + "devDependencies": { + "@better-auth/core": "^1.3.0", + "better-auth": "^1.3.0", + "@zenstackhq/eslint-config": "workspace:*", + "@zenstackhq/typescript-config": "workspace:*", + "@zenstackhq/vitest-config": "workspace:*" + } +} diff --git a/packages/auth-adapters/better-auth/src/adapter.ts b/packages/auth-adapters/better-auth/src/adapter.ts new file mode 100644 index 00000000..6a8f4a75 --- /dev/null +++ b/packages/auth-adapters/better-auth/src/adapter.ts @@ -0,0 +1,241 @@ +import type { BetterAuthOptions } from '@better-auth/core'; +import type { DBAdapter, DBAdapterDebugLogOption, Where } from '@better-auth/core/db/adapter'; +import { BetterAuthError } from '@better-auth/core/error'; +import type { ClientContract, ModelOperations, UpdateInput } from '@zenstackhq/orm'; +import type { GetModels, SchemaDef } from '@zenstackhq/orm/schema'; +import { + createAdapterFactory, + type AdapterFactoryCustomizeAdapterCreator, + type AdapterFactoryOptions, +} from 'better-auth/adapters'; +import { generateSchema } from './schema-generator'; + +/** + * Options for the ZenStack adapter factory. + */ +export interface AdapterConfig { + /** + * Database provider + */ + provider: 'sqlite' | 'postgresql'; + + /** + * Enable debug logs for the adapter + * + * @default false + */ + debugLogs?: DBAdapterDebugLogOption | undefined; + + /** + * Use plural table names + * + * @default false + */ + usePlural?: boolean | undefined; +} + +/** + * Create a Better-Auth adapter for ZenStack ORM. + * @param db ZenStack ORM client instance + * @param config adapter configuration options + */ +export const zenstackAdapter = (db: ClientContract, config: AdapterConfig) => { + let lazyOptions: BetterAuthOptions | null = null; + const createCustomAdapter = + (db: ClientContract): AdapterFactoryCustomizeAdapterCreator => + ({ getFieldName, options }) => { + const convertSelect = (select?: string[], model?: string) => { + if (!select || !model) return undefined; + return select.reduce((prev, cur) => { + return { + ...prev, + [getFieldName({ model, field: cur })]: true, + }; + }, {}); + }; + function operatorToORMOperator(operator: string) { + switch (operator) { + case 'starts_with': + return 'startsWith'; + case 'ends_with': + return 'endsWith'; + case 'ne': + return 'not'; + case 'not_in': + return 'notIn'; + default: + return operator; + } + } + const convertWhereClause = (model: string, where?: Where[]): any => { + if (!where || !where.length) return {}; + if (where.length === 1) { + const w = where[0]!; + if (!w) { + throw new BetterAuthError('Invalid where clause'); + } + return { + [getFieldName({ model, field: w.field })]: + w.operator === 'eq' || !w.operator + ? w.value + : { + [operatorToORMOperator(w.operator)]: w.value, + }, + }; + } + const and = where.filter((w) => w.connector === 'AND' || !w.connector); + const or = where.filter((w) => w.connector === 'OR'); + const andClause = and.map((w) => { + return { + [getFieldName({ model, field: w.field })]: + w.operator === 'eq' || !w.operator + ? w.value + : { + [operatorToORMOperator(w.operator)]: w.value, + }, + }; + }); + const orClause = or.map((w) => { + return { + [getFieldName({ model, field: w.field })]: + w.operator === 'eq' || !w.operator + ? w.value + : { + [operatorToORMOperator(w.operator)]: w.value, + }, + }; + }); + + return { + ...(andClause.length ? { AND: andClause } : {}), + ...(orClause.length ? { OR: orClause } : {}), + }; + }; + + function requireModelDb(db: ClientContract, model: string) { + const modelDb = db[model as keyof typeof db]; + if (!modelDb) { + throw new BetterAuthError( + `Model ${model} does not exist in the database. If you haven't generated the ZenStack schema, you need to run 'npx zen generate'`, + ); + } + return modelDb as unknown as ModelOperations>; + } + + return { + async create({ model, data: values, select }): Promise { + const modelDb = requireModelDb(db, model); + return await modelDb.create({ + data: values, + select: convertSelect(select, model), + }); + }, + + async findOne({ model, where, select }): Promise { + const modelDb = requireModelDb(db, model); + const whereClause = convertWhereClause(model, where); + return await modelDb.findFirst({ + where: whereClause, + select: convertSelect(select, model), + }); + }, + + async findMany({ model, where, limit, offset, sortBy }): Promise { + const modelDb = requireModelDb(db, model); + const whereClause = convertWhereClause(model, where); + return await modelDb.findMany({ + where: whereClause, + take: limit || 100, + skip: offset || 0, + ...(sortBy?.field + ? { + orderBy: { + [getFieldName({ model, field: sortBy.field })]: + sortBy.direction === 'desc' ? 'desc' : 'asc', + } as any, + } + : {}), + }); + }, + + async count({ model, where }) { + const modelDb = requireModelDb(db, model); + const whereClause = convertWhereClause(model, where); + return await modelDb.count({ + where: whereClause, + }); + }, + + async update({ model, where, update }): Promise { + const modelDb = requireModelDb(db, model); + const whereClause = convertWhereClause(model, where); + return await modelDb.update({ + where: whereClause, + data: update as UpdateInput>, + }); + }, + + async updateMany({ model, where, update }) { + const modelDb = requireModelDb(db, model); + const whereClause = convertWhereClause(model, where); + const result = await modelDb.updateMany({ + where: whereClause, + data: update, + }); + return result ? (result.count as number) : 0; + }, + + async delete({ model, where }): Promise { + const modelDb = requireModelDb(db, model); + const whereClause = convertWhereClause(model, where); + try { + await modelDb.delete({ + where: whereClause, + }); + } catch { + // If the record doesn't exist, we don't want to throw an error + } + }, + + async deleteMany({ model, where }) { + const modelDb = requireModelDb(db, model); + const whereClause = convertWhereClause(model, where); + const result = await modelDb.deleteMany({ + where: whereClause, + }); + return result ? (result.count as number) : 0; + }, + + options: config, + + createSchema: async ({ file, tables }) => { + return generateSchema(file, tables, config, options); + }, + }; + }; + + let adapterOptions: AdapterFactoryOptions | null = null; + adapterOptions = { + config: { + adapterId: 'zenstack', + adapterName: 'ZenStack Adapter', + usePlural: config.usePlural ?? false, + debugLogs: config.debugLogs ?? false, + transaction: (cb) => + db.$transaction((tx) => { + const adapter = createAdapterFactory({ + config: adapterOptions!.config, + adapter: createCustomAdapter(tx as ClientContract), + })(lazyOptions!); + return cb(adapter); + }), + }, + adapter: createCustomAdapter(db), + }; + + const adapter = createAdapterFactory(adapterOptions); + return (options: BetterAuthOptions): DBAdapter => { + lazyOptions = options; + return adapter(options); + }; +}; diff --git a/packages/auth-adapters/better-auth/src/index.ts b/packages/auth-adapters/better-auth/src/index.ts new file mode 100644 index 00000000..92d8b5cc --- /dev/null +++ b/packages/auth-adapters/better-auth/src/index.ts @@ -0,0 +1 @@ +export { zenstackAdapter, type AdapterConfig } from './adapter'; diff --git a/packages/auth-adapters/better-auth/src/schema-generator.ts b/packages/auth-adapters/better-auth/src/schema-generator.ts new file mode 100644 index 00000000..1ede4b26 --- /dev/null +++ b/packages/auth-adapters/better-auth/src/schema-generator.ts @@ -0,0 +1,546 @@ +import { lowerCaseFirst, upperCaseFirst } from '@zenstackhq/common-helpers'; +import { loadDocument, ZModelCodeGenerator } from '@zenstackhq/language'; +import { + Argument, + ArrayExpr, + AttributeArg, + BooleanLiteral, + ConfigExpr, + ConfigField, + DataField, + DataFieldAttribute, + DataFieldType, + DataModel, + DataModelAttribute, + DataSource, + InvocationExpr, + isDataModel, + Model, + ReferenceExpr, + StringLiteral, +} from '@zenstackhq/language/ast'; +import { hasAttribute } from '@zenstackhq/language/utils'; +import { type BetterAuthOptions } from 'better-auth'; +import type { DBAdapterSchemaCreation } from 'better-auth/adapters'; +import type { BetterAuthDBSchema, DBFieldAttribute, DBFieldType } from 'better-auth/db'; +import fs from 'node:fs'; +import path from 'node:path'; +import { match } from 'ts-pattern'; +import type { AdapterConfig } from './adapter'; + +export async function generateSchema( + file: string | undefined, + tables: BetterAuthDBSchema, + config: AdapterConfig, + options: BetterAuthOptions, +): Promise { + let filePath = file; + + if (!filePath) { + // TODO: respect "zenstack" entry in package.json for default schema file path + if (fs.existsSync('./schema.zmodel')) { + filePath = './schema.zmodel'; + } else { + filePath = './zenstack/schema.zmodel'; + } + } + + const schemaExists = !!(filePath && fs.existsSync(path.join(process.cwd(), filePath))); + + const schema = await updateSchema(filePath, tables, config, options); + + return { + code: schema ?? '', + path: filePath, + overwrite: schemaExists && !!schema, + }; +} + +async function updateSchema( + schemaPath: string, + tables: BetterAuthDBSchema, + config: AdapterConfig, + options: BetterAuthOptions, +) { + let zmodel: Model | undefined; + if (fs.existsSync(schemaPath)) { + const loadResult = await loadDocument(schemaPath); + if (!loadResult.success) { + throw new Error(`Failed to load existing schema at ${schemaPath}: ${loadResult.errors.join(', ')}`); + } + zmodel = loadResult.model; + } else { + zmodel = initializeZmodel(config); + } + + // collect to-many relations + const toManyRelations = new Map(); + for (const [tableName, table] of Object.entries(tables)) { + const fields = tables[tableName]?.fields; + for (const field in fields) { + const attr = fields[field]!; + if (attr.references) { + const referencedOriginalModel = attr.references.model; + const referencedCustomModel = tables[referencedOriginalModel]?.modelName || referencedOriginalModel; + const referencedModelNameCap = upperCaseFirst(referencedCustomModel); + if (!toManyRelations.has(referencedModelNameCap)) { + toManyRelations.set(referencedModelNameCap, new Set()); + } + const currentCustomModel = table.modelName ?? tableName; + const currentModelNameCap = upperCaseFirst(currentCustomModel); + toManyRelations.get(referencedModelNameCap).add(currentModelNameCap); + } + } + } + + let changed = false; + + for (const [name, table] of Object.entries(tables)) { + const c = addOrUpdateModel( + name, + table, + zmodel, + tables, + toManyRelations, + !!options.advanced?.database?.useNumberId, + ); + changed = changed || c; + } + + if (!changed) { + return undefined; + } + + const generator = new ZModelCodeGenerator(); + const content = generator.generate(zmodel); + + return content; +} + +// @default(now()) +function addDefaultNow(df: DataField) { + const nowArg: AttributeArg = { + $type: 'AttributeArg', + } as any; + const nowExpr: InvocationExpr = { + $type: 'InvocationExpr', + function: { $refText: 'now' }, + args: [], + $container: nowArg, + }; + nowArg.value = nowExpr; + addFieldAttribute(df, '@default', [nowArg]); +} + +function createDataModel(modelName: string, zmodel: Model, numericId: boolean) { + const dataModel: DataModel = { + $type: 'DataModel', + name: modelName, + fields: [], + attributes: [], + mixins: [], + comments: [], + isView: false, + $container: zmodel, + }; + + let idField: DataField; + if (numericId) { + idField = addModelField(dataModel, 'id', 'Int', false, false); + } else { + idField = addModelField(dataModel, 'id', 'String', false, false); + } + addFieldAttribute(idField, '@id'); + + return dataModel; +} + +function addModelField(dataModel: DataModel, fieldName: string, fieldType: string, array: boolean, optional: boolean) { + const idField: DataField = { + $type: 'DataField', + name: fieldName, + attributes: [], + comments: [], + $container: dataModel, + } as any; + idField.type = { + $type: 'DataFieldType', + type: fieldType as any, + array, + optional, + $container: idField, + }; + dataModel.fields.push(idField); + return idField; +} + +function initializeZmodel(config: AdapterConfig) { + const zmodel: Model = { + $type: 'Model', + declarations: [], + imports: [], + }; + + // datasource db { ... } + const ds: DataSource = { + $type: 'DataSource', + name: 'db', + fields: [], + $container: zmodel, + }; + zmodel.declarations.push(ds); + + // provider = 'sqlite' | 'postgresql' + const providerField: ConfigField = { + $type: 'ConfigField', + name: 'provider', + $container: ds, + } as any; + providerField.value = { + $type: 'StringLiteral', + value: config.provider, + $container: providerField, + } satisfies ConfigExpr; + + const urlField: ConfigField = { + $type: 'ConfigField', + name: 'url', + $container: ds, + } as any; + + // env('DATABASE_URL') + const envCall: InvocationExpr = { + $type: 'InvocationExpr', + function: { + $refText: 'env', + }, + args: [], + $container: urlField, + }; + + // 'DATABASE_URL' arg + const dbUrlArg: Argument = { + $type: 'Argument', + } as any; + dbUrlArg.value = { + $type: 'StringLiteral', + value: 'DATABASE_URL', + $container: dbUrlArg, + } satisfies ConfigExpr; + + envCall.args = [dbUrlArg]; + + urlField.value = + config.provider === 'sqlite' + ? { + $type: 'StringLiteral', + value: 'file:./test.db', + $container: urlField, + } + : envCall; + + ds.fields.push(providerField); + ds.fields.push(urlField); + + return zmodel; +} + +function getMappedFieldType({ bigint, type }: DBFieldAttribute) { + return match(type) + .with('string', () => ({ type: 'String' })) + .with('number', () => (bigint ? { type: 'BigInt' } : { type: 'Int' })) + .with('boolean', () => ({ type: 'Boolean' })) + .with('date', () => ({ type: 'DateTime' })) + .with('json', () => ({ type: 'Json' })) + .with('string[]', () => ({ type: 'String', array: true })) + .with('number[]', () => ({ type: 'Int', array: true })) + .otherwise(() => { + throw new Error(`Unsupported field type: ${type}`); + }); +} + +function addOrUpdateModel( + tableName: string, + table: BetterAuthDBSchema[string], + zmodel: Model, + tables: BetterAuthDBSchema, + toManyRelations: Map>, + numericId: boolean, +): boolean { + let changed = false; + const customModelName = tables[tableName]?.modelName ?? tableName; + const modelName = upperCaseFirst(customModelName); + + let dataModel = zmodel.declarations.find((d): d is DataModel => isDataModel(d) && d.name === modelName); + if (!dataModel) { + changed = true; + dataModel = createDataModel(modelName, zmodel, numericId); + zmodel.declarations.push(dataModel); + } + + if (modelName !== tableName && !hasAttribute(dataModel, '@@map')) { + addModelAttribute(dataModel, '@@map', [createStringAttributeArg(tableName)]); + } + + for (const [fName, field] of Object.entries(table.fields)) { + if (dataModel.fields.some((f) => f.name === fName)) { + continue; + } + + const fieldName = field.fieldName ?? fName; + changed = true; + + if (!field.references) { + // scalar field + const { array, type } = getMappedFieldType(field); + + const df: DataField = { + $type: 'DataField', + name: fieldName, + attributes: [], + comments: [], + $container: dataModel, + } as any; + df.type = { + $type: 'DataFieldType', + type: type as any, + array: !!array, + optional: !field.required, + $container: df, + }; + dataModel.fields.push(df); + + // @id + if (fieldName === 'id') { + addFieldAttribute(df, '@id'); + } + + // @unique + if (field.unique) { + addFieldAttribute(df, '@unique'); + } + + // @default + if (field.defaultValue !== undefined) { + if (fieldName === 'createdAt') { + // @default(now()) + addDefaultNow(df); + } else if (typeof field.defaultValue === 'boolean') { + addFieldAttribute(df, '@default', [createBooleanAttributeArg(field.defaultValue)]); + } else if (typeof field.defaultValue === 'function') { + // For other function-based defaults, we'll need to check what they return + const defaultVal = field.defaultValue(); + if (defaultVal instanceof Date) { + // @default(now()) + addDefaultNow(df); + } else { + console.warn( + `Warning: Unsupported default function for field ${fieldName} in model ${fName}. Please adjust manually.`, + ); + } + } + } + + // This is a special handling for updatedAt fields + if (fieldName === 'updatedAt' && field.onUpdate) { + addFieldAttribute(df, '@updatedAt'); + } else if (field.onUpdate) { + console.warn( + `Warning: 'onUpdate' is only supported on 'updatedAt' fields. Please adjust manually for field ${fieldName} in model ${table.modelName}.`, + ); + } + } else { + // relation + + // fk field + addModelField(dataModel, fieldName, numericId ? 'Int' : 'String', false, !field.required); + + // relation field + const referencedOriginalModelName = field.references.model; + const referencedCustomModelName = + tables[referencedOriginalModelName]?.modelName || referencedOriginalModelName; + + const relationField: DataField = { + $type: 'DataField', + name: lowerCaseFirst(referencedCustomModelName), + attributes: [], + comments: [], + $container: dataModel, + } as any; + relationField.type = { + $type: 'DataFieldType', + reference: { + $refText: upperCaseFirst(referencedCustomModelName), + }, + array: (field.type as string).endsWith('[]'), + optional: !field.required, + $container: relationField, + } satisfies DataFieldType; + + let action = 'Cascade'; + if (field.references.onDelete === 'no action') action = 'NoAction'; + else if (field.references.onDelete === 'set null') action = 'SetNull'; + else if (field.references.onDelete === 'set default') action = 'SetDefault'; + else if (field.references.onDelete === 'restrict') action = 'Restrict'; + + // @relation(fields: [field], references: [referencedField], onDelete: action) + const relationAttr: DataFieldAttribute = { + $type: 'DataFieldAttribute', + decl: { + $refText: '@relation', + }, + args: [], + $container: relationField, + }; + + // fields: [field] + const fieldsArg: AttributeArg = { + $type: 'AttributeArg', + name: 'fields', + $container: relationAttr, + } as any; + const fieldsExpr: ArrayExpr = { + $type: 'ArrayExpr', + items: [], + $container: fieldsArg, + }; + const fkRefExpr: ReferenceExpr = { + $type: 'ReferenceExpr', + args: [], + $container: fieldsExpr, + target: { + $refText: fieldName, + }, + }; + fieldsExpr.items.push(fkRefExpr); + fieldsArg.value = fieldsExpr; + + // references: [referencedField] + const referencesArg: AttributeArg = { + $type: 'AttributeArg', + name: 'references', + $container: relationAttr, + } as any; + const referencesExpr: ArrayExpr = { + $type: 'ArrayExpr', + items: [], + $container: referencesArg, + }; + const pkRefExpr: ReferenceExpr = { + $type: 'ReferenceExpr', + args: [], + $container: referencesExpr, + target: { + $refText: field.references.field, + }, + }; + referencesExpr.items.push(pkRefExpr); + referencesArg.value = referencesExpr; + + // onDelete: action + const onDeleteArg: AttributeArg = { + $type: 'AttributeArg', + name: 'onDelete', + $container: relationAttr, + } as any; + const onDeleteValueExpr: ReferenceExpr = { + $type: 'ReferenceExpr', + target: { $refText: action }, + args: [], + $container: onDeleteArg, + }; + onDeleteArg.value = onDeleteValueExpr; + + relationAttr.args.push(...[fieldsArg, referencesArg, onDeleteArg]); + relationField.attributes.push(relationAttr); + + dataModel.fields.push(relationField); + } + } + + // add to-many relations + if (toManyRelations.has(modelName)) { + const relations = toManyRelations.get(modelName)!; + for (const relatedModel of relations) { + const relationName = `${lowerCaseFirst(relatedModel)}s`; + if (!dataModel.fields.some((f) => f.name === relationName)) { + const relationField: DataField = { + $type: 'DataField', + name: relationName, + attributes: [], + comments: [], + $container: dataModel, + } as any; + const relationType: DataFieldType = { + $type: 'DataFieldType', + reference: { + $refText: relatedModel, + }, + array: true, + optional: false, + $container: relationField, + }; + relationField.type = relationType; + dataModel.fields.push(relationField); + } + } + } + + return changed; +} + +function addModelAttribute(dataModel: DataModel, name: string, args: Omit[] = []) { + const attr: DataModelAttribute = { + $type: 'DataModelAttribute', + decl: { $refText: name }, + $container: dataModel, + args: [], + }; + const finalArgs = args.map((arg) => ({ + ...arg, + $container: attr, + })); + attr.args.push(...finalArgs); + dataModel.attributes.push(attr); +} + +function addFieldAttribute(dataField: DataField, name: string, args: Omit[] = []) { + const attr: DataFieldAttribute = { + $type: 'DataFieldAttribute', + decl: { $refText: name }, + $container: dataField, + args: [], + }; + const finalArgs = args.map((arg) => ({ + ...arg, + $container: attr, + })); + attr.args.push(...finalArgs); + dataField.attributes.push(attr); +} + +function createBooleanAttributeArg(value: boolean) { + const arg: AttributeArg = { + $type: 'AttributeArg', + } as any; + const expr: BooleanLiteral = { + $type: 'BooleanLiteral', + value, + $container: arg, + }; + arg.value = expr; + return arg; +} + +function createStringAttributeArg(value: string) { + const arg: AttributeArg = { + $type: 'AttributeArg', + } as any; + const expr: StringLiteral = { + $type: 'StringLiteral', + value, + $container: arg, + }; + arg.value = expr; + return arg; +} diff --git a/packages/auth-adapters/better-auth/tsconfig.json b/packages/auth-adapters/better-auth/tsconfig.json new file mode 100644 index 00000000..8784ef54 --- /dev/null +++ b/packages/auth-adapters/better-auth/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@zenstackhq/typescript-config/base.json", + "compilerOptions": { + "rootDir": ".", + "noPropertyAccessFromIndexSignature": false + }, + "include": ["src/**/*"] +} diff --git a/packages/auth-adapters/better-auth/tsup.config.ts b/packages/auth-adapters/better-auth/tsup.config.ts new file mode 100644 index 00000000..5a74a9dd --- /dev/null +++ b/packages/auth-adapters/better-auth/tsup.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: { + index: 'src/index.ts', + }, + outDir: 'dist', + splitting: false, + sourcemap: true, + clean: true, + dts: true, + format: ['cjs', 'esm'], +}); diff --git a/packages/auth-adapters/better-auth/vitest.config.ts b/packages/auth-adapters/better-auth/vitest.config.ts new file mode 100644 index 00000000..75a9f709 --- /dev/null +++ b/packages/auth-adapters/better-auth/vitest.config.ts @@ -0,0 +1,4 @@ +import base from '@zenstackhq/vitest-config/base'; +import { defineConfig, mergeConfig } from 'vitest/config'; + +export default mergeConfig(base, defineConfig({})); diff --git a/packages/cli/src/actions/action-utils.ts b/packages/cli/src/actions/action-utils.ts index fa2bb0a5..8a0ca741 100644 --- a/packages/cli/src/actions/action-utils.ts +++ b/packages/cli/src/actions/action-utils.ts @@ -30,10 +30,10 @@ export function getSchemaFile(file?: string) { } } - if (fs.existsSync('./zenstack/schema.zmodel')) { - return './zenstack/schema.zmodel'; - } else if (fs.existsSync('./schema.zmodel')) { + if (fs.existsSync('./schema.zmodel')) { return './schema.zmodel'; + } else if (fs.existsSync('./zenstack/schema.zmodel')) { + return './zenstack/schema.zmodel'; } else { throw new CliError( 'Schema file not found in default locations ("./zenstack/schema.zmodel" or "./schema.zmodel").', diff --git a/packages/language/src/zmodel-code-generator.ts b/packages/language/src/zmodel-code-generator.ts index 2a63969f..55efb5fc 100644 --- a/packages/language/src/zmodel-code-generator.ts +++ b/packages/language/src/zmodel-code-generator.ts @@ -41,7 +41,6 @@ import { UnaryExpr, type AstNode, } from './ast'; -import { resolved } from './utils'; /** * Options for the generator. @@ -161,7 +160,7 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')} @gen(DataModel) private _generateDataModel(ast: DataModel) { return `${ast.isView ? 'view' : 'model'} ${ast.name}${ - ast.mixins.length > 0 ? ' mixes ' + ast.mixins.map((x) => x.ref?.name).join(', ') : '' + ast.mixins.length > 0 ? ' mixes ' + ast.mixins.map((x) => x.$refText).join(', ') : '' } { ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ ast.attributes.length > 0 @@ -199,7 +198,7 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ private attribute(ast: DataModelAttribute | DataFieldAttribute) { const args = ast.args.length ? `(${ast.args.map((x) => this.generate(x)).join(', ')})` : ''; - return `${resolved(ast.decl).name}${args}`; + return `${ast.decl.$refText}${args}`; } @gen(AttributeArg) @@ -265,7 +264,7 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ @gen(ReferenceExpr) private _generateReferenceExpr(ast: ReferenceExpr) { const args = ast.args.length ? `(${ast.args.map((x) => this.generate(x)).join(', ')})` : ''; - return `${ast.target.ref?.name}${args}`; + return `${ast.target.$refText}${args}`; } @gen(ReferenceArg) @@ -275,12 +274,12 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ @gen(MemberAccessExpr) private _generateMemberExpr(ast: MemberAccessExpr) { - return `${this.generate(ast.operand)}.${ast.member.ref?.name}`; + return `${this.generate(ast.operand)}.${ast.member.$refText}`; } @gen(InvocationExpr) private _generateInvocationExpr(ast: InvocationExpr) { - return `${ast.function.ref?.name}(${ast.args.map((x) => this.argument(x)).join(', ')})`; + return `${ast.function.$refText}(${ast.args.map((x) => this.argument(x)).join(', ')})`; } @gen(NullExpr) diff --git a/packages/orm/src/client/crud-types.ts b/packages/orm/src/client/crud-types.ts index 7f5defdf..934c0139 100644 --- a/packages/orm/src/client/crud-types.ts +++ b/packages/orm/src/client/crud-types.ts @@ -778,15 +778,20 @@ type ConnectOrCreatePayload< create: CreateInput; }; -type CreateManyInput, Without extends string = never> = { +export type CreateManyInput< + Schema extends SchemaDef, + Model extends GetModels, + Without extends string = never, +> = { data: OrArray, Without> & Omit, Without>>; skipDuplicates?: boolean; }; -type CreateInput, Without extends string = never> = XOR< - Omit, Without>, - Omit, Without> ->; +export type CreateInput< + Schema extends SchemaDef, + Model extends GetModels, + Without extends string = never, +> = XOR, Without>, Omit, Without>>; type NestedCreateInput< Schema extends SchemaDef, @@ -882,7 +887,7 @@ type UpdateRelationInput< Without >; -type UpdateInput< +export type UpdateInput< Schema extends SchemaDef, Model extends GetModels, Without extends string = never, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b01cfd8..4eb5eaf2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -120,6 +120,37 @@ importers: specifier: ^2.8.0 version: 2.8.0 + packages/auth-adapters/better-auth: + dependencies: + '@zenstackhq/common-helpers': + specifier: workspace:* + version: link:../../common-helpers + '@zenstackhq/language': + specifier: workspace:* + version: link:../../language + '@zenstackhq/orm': + specifier: workspace:* + version: link:../../orm + ts-pattern: + specifier: 'catalog:' + version: 5.7.1 + devDependencies: + '@better-auth/core': + specifier: ^1.3.0 + version: 1.3.34(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.19)(jose@6.1.2)(kysely@0.28.8)(nanostores@1.0.1) + '@zenstackhq/eslint-config': + specifier: workspace:* + version: link:../../config/eslint-config + '@zenstackhq/typescript-config': + specifier: workspace:* + version: link:../../config/typescript-config + '@zenstackhq/vitest-config': + specifier: workspace:* + version: link:../../config/vitest-config + better-auth: + specifier: ^1.3.0 + version: 1.3.34 + packages/cli: dependencies: '@zenstackhq/common-helpers': @@ -989,6 +1020,25 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} + '@better-auth/core@1.3.34': + resolution: {integrity: sha512-rt/Bgl0Xa8OQ2DUMKCZEJ8vL9kUw4NCJsBP9Sj9uRhbsK8NEMPiznUOFMkUY2FvrslvfKN7H/fivwyHz9c7HzQ==} + peerDependencies: + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.18 + better-call: 1.0.19 + jose: ^6.1.0 + kysely: ^0.28.5 + nanostores: ^1.0.1 + + '@better-auth/telemetry@1.3.34': + resolution: {integrity: sha512-aQZ3wN90YMqV49diWxAMe1k7s2qb55KCsedCZne5PlgCjU4s3YtnqyjC5FEpzw2KY8l8rvR7DMAsDl13NjObKA==} + + '@better-auth/utils@0.3.0': + resolution: {integrity: sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==} + + '@better-fetch/fetch@1.1.18': + resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==} + '@borewit/text-codec@0.1.1': resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==} @@ -1424,6 +1474,9 @@ packages: '@fastify/proxy-addr@5.1.0': resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + '@hexagon/base64@1.1.28': + resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1620,6 +1673,9 @@ packages: '@kwsites/promise-deferred@1.1.1': resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} + '@levischuck/tiny-cbor@0.2.11': + resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==} + '@mapbox/node-pre-gyp@2.0.0': resolution: {integrity: sha512-llMXd39jtP0HpQLVI37Bf1m2ADlEb35GYSh1SDSLsBhR+5iCxiNGlT31yqbNtVHygHAtMy6dWFERpU2JgufhPg==} engines: {node: '>=18'} @@ -1689,10 +1745,18 @@ packages: cpu: [x64] os: [win32] + '@noble/ciphers@2.0.1': + resolution: {integrity: sha512-xHK3XHPUW8DTAobU+G0XT+/w+JLM7/8k1UFdB5xg/zTFPnFCobhftzw8wl4Lw2aq/Rvir5pxfZV5fEazmeCJ2g==} + engines: {node: '>= 20.19.0'} + '@noble/hashes@1.7.1': resolution: {integrity: sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==} engines: {node: ^14.21.3 || >=16} + '@noble/hashes@2.0.1': + resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} + engines: {node: '>= 20.19.0'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2136,6 +2200,43 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} + '@peculiar/asn1-android@2.6.0': + resolution: {integrity: sha512-cBRCKtYPF7vJGN76/yG8VbxRcHLPF3HnkoHhKOZeHpoVtbMYfY9ROKtH3DtYUY9m8uI1Mh47PRhHf2hSK3xcSQ==} + + '@peculiar/asn1-cms@2.6.0': + resolution: {integrity: sha512-2uZqP+ggSncESeUF/9Su8rWqGclEfEiz1SyU02WX5fUONFfkjzS2Z/F1Li0ofSmf4JqYXIOdCAZqIXAIBAT1OA==} + + '@peculiar/asn1-csr@2.6.0': + resolution: {integrity: sha512-BeWIu5VpTIhfRysfEp73SGbwjjoLL/JWXhJ/9mo4vXnz3tRGm+NGm3KNcRzQ9VMVqwYS2RHlolz21svzRXIHPQ==} + + '@peculiar/asn1-ecc@2.6.0': + resolution: {integrity: sha512-FF3LMGq6SfAOwUG2sKpPXblibn6XnEIKa+SryvUl5Pik+WR9rmRA3OCiwz8R3lVXnYnyRkSZsSLdml8H3UiOcw==} + + '@peculiar/asn1-pfx@2.6.0': + resolution: {integrity: sha512-rtUvtf+tyKGgokHHmZzeUojRZJYPxoD/jaN1+VAB4kKR7tXrnDCA/RAWXAIhMJJC+7W27IIRGe9djvxKgsldCQ==} + + '@peculiar/asn1-pkcs8@2.6.0': + resolution: {integrity: sha512-KyQ4D8G/NrS7Fw3XCJrngxmjwO/3htnA0lL9gDICvEQ+GJ+EPFqldcJQTwPIdvx98Tua+WjkdKHSC0/Km7T+lA==} + + '@peculiar/asn1-pkcs9@2.6.0': + resolution: {integrity: sha512-b78OQ6OciW0aqZxdzliXGYHASeCvvw5caqidbpQRYW2mBtXIX2WhofNXTEe7NyxTb0P6J62kAAWLwn0HuMF1Fw==} + + '@peculiar/asn1-rsa@2.6.0': + resolution: {integrity: sha512-Nu4C19tsrTsCp9fDrH+sdcOKoVfdfoQQ7S3VqjJU6vedR7tY3RLkQ5oguOIB3zFW33USDUuYZnPEQYySlgha4w==} + + '@peculiar/asn1-schema@2.6.0': + resolution: {integrity: sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==} + + '@peculiar/asn1-x509-attr@2.6.0': + resolution: {integrity: sha512-MuIAXFX3/dc8gmoZBkwJWxUWOSvG4MMDntXhrOZpJVMkYX+MYc/rUAU2uJOved9iJEoiUx7//3D8oG83a78UJA==} + + '@peculiar/asn1-x509@2.6.0': + resolution: {integrity: sha512-uzYbPEpoQiBoTq0/+jZtpM6Gq6zADBx+JNFP3yqRgziWBxQ/Dt/HcuvRfm9zJTPdRcBqPNdaRHTVwpyiq6iNMA==} + + '@peculiar/x509@1.14.2': + resolution: {integrity: sha512-r2w1Hg6pODDs0zfAKHkSS5HLkOLSeburtcgwvlLLWWCixw+MmW3U6kD5ddyvc2Y2YdbGuVwCF2S2ASoU1cFAag==} + engines: {node: '>=22.0.0'} + '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -2467,6 +2568,13 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@simplewebauthn/browser@13.2.2': + resolution: {integrity: sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA==} + + '@simplewebauthn/server@13.2.2': + resolution: {integrity: sha512-HcWLW28yTMGXpwE9VLx9J+N2KEUaELadLrkPEEI9tpI5la70xNEVEsu/C+m3u7uoq4FulLqZQhgBCzR9IZhFpA==} + engines: {node: '>=20.0.0'} + '@sinclair/typebox@0.34.41': resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==} @@ -3349,6 +3457,10 @@ packages: asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + asn1js@3.0.6: + resolution: {integrity: sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==} + engines: {node: '>=12.0.0'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -3429,6 +3541,38 @@ packages: resolution: {integrity: sha512-JU0h5APyQNsHOlAM7HnQnPToSDQoEBZqzu/YBlqDnEeymPnZDREeXJA3KBMQee+dKteAxZ2AtvQEvVYdZf241Q==} hasBin: true + better-auth@1.3.34: + resolution: {integrity: sha512-LWA52SlvnUBJRbN8VLSTLILPomZY3zZAiLxVJCeSQ5uVmaIKkMBhERitkfJcXB9RJcfl4uP+3EqKkb6hX1/uiw==} + peerDependencies: + '@lynx-js/react': '*' + '@sveltejs/kit': '*' + next: '*' + react: '*' + react-dom: '*' + solid-js: '*' + svelte: '*' + vue: '*' + peerDependenciesMeta: + '@lynx-js/react': + optional: true + '@sveltejs/kit': + optional: true + next: + optional: true + react: + optional: true + react-dom: + optional: true + solid-js: + optional: true + svelte: + optional: true + vue: + optional: true + + better-call@1.0.19: + resolution: {integrity: sha512-sI3GcA1SCVa3H+CDHl8W8qzhlrckwXOTKhqq3OOPXjgn5aTOMIqGY34zLY/pHA6tRRMjTUC3lz5Mi7EbDA24Kw==} + better-sqlite3@12.2.0: resolution: {integrity: sha512-eGbYq2CT+tos1fBwLQ/tkBt9J5M3JEHjku4hbvQUePCckkvVf14xWj+1m7dGoK81M/fOjFT7yM9UMeKT/+vFLQ==} engines: {node: 20.x || 22.x || 23.x || 24.x} @@ -4932,6 +5076,9 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jose@6.1.2: + resolution: {integrity: sha512-MpcPtHLE5EmztuFIqB0vzHAWJPpmN1E6L4oo+kze56LIs3MyXIj9ZHMDxqOvkP38gBR7K1v3jqd4WU2+nrfONQ==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -5388,6 +5535,10 @@ packages: engines: {node: ^18 || >=20} hasBin: true + nanostores@1.0.1: + resolution: {integrity: sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw==} + engines: {node: ^20.0.0 || >=22.0.0} + nanotar@0.2.0: resolution: {integrity: sha512-9ca1h0Xjvo9bEkE4UOxgAzLV0jHKe6LMaxo37ND2DAhhAtd0j8pR1Wxz+/goMrZO8AEZTWCmyaOsFI/W5AdpCQ==} @@ -6108,6 +6259,13 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + pvtsutils@1.3.6: + resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} + + pvutils@1.1.5: + resolution: {integrity: sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==} + engines: {node: '>=16.0.0'} + qs@6.13.0: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} @@ -6194,6 +6352,9 @@ packages: resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} engines: {node: '>=4'} + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -6275,6 +6436,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rou3@0.5.1: + resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==} + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -6805,6 +6969,9 @@ packages: tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -6832,6 +6999,10 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + tsyringe@4.10.0: + resolution: {integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==} + engines: {node: '>= 6.0.0'} + tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} @@ -7711,6 +7882,31 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@better-auth/core@1.3.34(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.19)(jose@6.1.2)(kysely@0.28.8)(nanostores@1.0.1)': + dependencies: + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.18 + better-call: 1.0.19 + jose: 6.1.2 + kysely: 0.28.8 + nanostores: 1.0.1 + zod: 4.1.12 + + '@better-auth/telemetry@1.3.34(better-call@1.0.19)(jose@6.1.2)(kysely@0.28.8)(nanostores@1.0.1)': + dependencies: + '@better-auth/core': 1.3.34(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.19)(jose@6.1.2)(kysely@0.28.8)(nanostores@1.0.1) + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.18 + transitivePeerDependencies: + - better-call + - jose + - kysely + - nanostores + + '@better-auth/utils@0.3.0': {} + + '@better-fetch/fetch@1.1.18': {} + '@borewit/text-codec@0.1.1': {} '@chevrotain/cst-dts-gen@11.0.3': @@ -8014,6 +8210,8 @@ snapshots: '@fastify/forwarded': 3.0.1 ipaddr.js: 2.2.0 + '@hexagon/base64@1.1.28': {} + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -8178,6 +8376,8 @@ snapshots: '@kwsites/promise-deferred@1.1.1': {} + '@levischuck/tiny-cbor@0.2.11': {} + '@mapbox/node-pre-gyp@2.0.0': dependencies: consola: 3.4.2 @@ -8244,8 +8444,12 @@ snapshots: '@next/swc-win32-x64-msvc@16.0.1': optional: true + '@noble/ciphers@2.0.1': {} + '@noble/hashes@1.7.1': {} + '@noble/hashes@2.0.1': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -8800,6 +9004,102 @@ snapshots: '@parcel/watcher-win32-ia32': 2.5.1 '@parcel/watcher-win32-x64': 2.5.1 + '@peculiar/asn1-android@2.6.0': + dependencies: + '@peculiar/asn1-schema': 2.6.0 + asn1js: 3.0.6 + tslib: 2.8.1 + + '@peculiar/asn1-cms@2.6.0': + dependencies: + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + '@peculiar/asn1-x509-attr': 2.6.0 + asn1js: 3.0.6 + tslib: 2.8.1 + + '@peculiar/asn1-csr@2.6.0': + dependencies: + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + asn1js: 3.0.6 + tslib: 2.8.1 + + '@peculiar/asn1-ecc@2.6.0': + dependencies: + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + asn1js: 3.0.6 + tslib: 2.8.1 + + '@peculiar/asn1-pfx@2.6.0': + dependencies: + '@peculiar/asn1-cms': 2.6.0 + '@peculiar/asn1-pkcs8': 2.6.0 + '@peculiar/asn1-rsa': 2.6.0 + '@peculiar/asn1-schema': 2.6.0 + asn1js: 3.0.6 + tslib: 2.8.1 + + '@peculiar/asn1-pkcs8@2.6.0': + dependencies: + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + asn1js: 3.0.6 + tslib: 2.8.1 + + '@peculiar/asn1-pkcs9@2.6.0': + dependencies: + '@peculiar/asn1-cms': 2.6.0 + '@peculiar/asn1-pfx': 2.6.0 + '@peculiar/asn1-pkcs8': 2.6.0 + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + '@peculiar/asn1-x509-attr': 2.6.0 + asn1js: 3.0.6 + tslib: 2.8.1 + + '@peculiar/asn1-rsa@2.6.0': + dependencies: + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + asn1js: 3.0.6 + tslib: 2.8.1 + + '@peculiar/asn1-schema@2.6.0': + dependencies: + asn1js: 3.0.6 + pvtsutils: 1.3.6 + tslib: 2.8.1 + + '@peculiar/asn1-x509-attr@2.6.0': + dependencies: + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + asn1js: 3.0.6 + tslib: 2.8.1 + + '@peculiar/asn1-x509@2.6.0': + dependencies: + '@peculiar/asn1-schema': 2.6.0 + asn1js: 3.0.6 + pvtsutils: 1.3.6 + tslib: 2.8.1 + + '@peculiar/x509@1.14.2': + dependencies: + '@peculiar/asn1-cms': 2.6.0 + '@peculiar/asn1-csr': 2.6.0 + '@peculiar/asn1-ecc': 2.6.0 + '@peculiar/asn1-pkcs9': 2.6.0 + '@peculiar/asn1-rsa': 2.6.0 + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + pvtsutils: 1.3.6 + reflect-metadata: 0.2.2 + tslib: 2.8.1 + tsyringe: 4.10.0 + '@pinojs/redact@0.4.0': {} '@pkgjs/parseargs@0.11.0': @@ -9046,6 +9346,19 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} + '@simplewebauthn/browser@13.2.2': {} + + '@simplewebauthn/server@13.2.2': + dependencies: + '@hexagon/base64': 1.1.28 + '@levischuck/tiny-cbor': 0.2.11 + '@peculiar/asn1-android': 2.6.0 + '@peculiar/asn1-ecc': 2.6.0 + '@peculiar/asn1-rsa': 2.6.0 + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + '@peculiar/x509': 1.14.2 + '@sinclair/typebox@0.34.41': {} '@sindresorhus/is@7.1.0': {} @@ -10062,6 +10375,12 @@ snapshots: asap@2.0.6: {} + asn1js@3.0.6: + dependencies: + pvtsutils: 1.3.6 + pvutils: 1.1.5 + tslib: 2.8.1 + assertion-error@2.0.1: {} ast-kit@2.1.3: @@ -10119,6 +10438,31 @@ snapshots: baseline-browser-mapping@2.8.21: {} + better-auth@1.3.34: + dependencies: + '@better-auth/core': 1.3.34(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.19)(jose@6.1.2)(kysely@0.28.8)(nanostores@1.0.1) + '@better-auth/telemetry': 1.3.34(better-call@1.0.19)(jose@6.1.2)(kysely@0.28.8)(nanostores@1.0.1) + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.18 + '@noble/ciphers': 2.0.1 + '@noble/hashes': 2.0.1 + '@simplewebauthn/browser': 13.2.2 + '@simplewebauthn/server': 13.2.2 + better-call: 1.0.19 + defu: 6.1.4 + jose: 6.1.2 + kysely: 0.28.8 + nanostores: 1.0.1 + zod: 4.1.12 + + better-call@1.0.19: + dependencies: + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.18 + rou3: 0.5.1 + set-cookie-parser: 2.7.2 + uncrypto: 0.1.3 + better-sqlite3@12.2.0: dependencies: bindings: 1.5.0 @@ -11888,6 +12232,8 @@ snapshots: jiti@2.6.1: {} + jose@6.1.2: {} + joycon@3.1.1: {} js-tokens@4.0.0: {} @@ -12319,6 +12665,8 @@ snapshots: nanoid@5.1.6: {} + nanostores@1.0.1: {} + nanotar@0.2.0: {} napi-build-utils@2.0.0: {} @@ -13257,6 +13605,12 @@ snapshots: pure-rand@6.1.0: {} + pvtsutils@1.3.6: + dependencies: + tslib: 2.8.1 + + pvutils@1.1.5: {} + qs@6.13.0: dependencies: side-channel: 1.1.0 @@ -13349,6 +13703,8 @@ snapshots: dependencies: redis-errors: 1.2.0 + reflect-metadata@0.2.2: {} + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -13469,6 +13825,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.52.5 fsevents: 2.3.3 + rou3@0.5.1: {} + router@2.2.0: dependencies: debug: 4.4.1 @@ -14095,6 +14453,8 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 + tslib@1.14.1: {} + tslib@2.8.1: {} tsup@8.5.0(@swc/core@1.12.5)(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0): @@ -14133,6 +14493,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tsyringe@4.10.0: + dependencies: + tslib: 1.14.1 + tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 From 167e185db74cb783ac797a0b9c25093952a17c6b Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:47:00 -0800 Subject: [PATCH 2/2] address pr comments --- packages/auth-adapters/better-auth/package.json | 4 ++-- packages/auth-adapters/better-auth/src/adapter.ts | 3 +-- .../auth-adapters/better-auth/src/schema-generator.ts | 9 ++++----- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/auth-adapters/better-auth/package.json b/packages/auth-adapters/better-auth/package.json index acf41118..38f998f0 100644 --- a/packages/auth-adapters/better-auth/package.json +++ b/packages/auth-adapters/better-auth/package.json @@ -41,8 +41,8 @@ "ts-pattern": "catalog:" }, "peerDependencies": { - "@better-auth/core": "^1.0.0", - "better-auth": "^1.0.0" + "@better-auth/core": "^1.3.0", + "better-auth": "^1.3.0" }, "devDependencies": { "@better-auth/core": "^1.3.0", diff --git a/packages/auth-adapters/better-auth/src/adapter.ts b/packages/auth-adapters/better-auth/src/adapter.ts index 6a8f4a75..935f5fde 100644 --- a/packages/auth-adapters/better-auth/src/adapter.ts +++ b/packages/auth-adapters/better-auth/src/adapter.ts @@ -214,8 +214,7 @@ export const zenstackAdapter = (db: ClientContract f.name === fName)) { + const fieldName = field.fieldName ?? fName; + if (dataModel.fields.some((f) => f.name === fieldName)) { continue; } - const fieldName = field.fieldName ?? fName; changed = true; if (!field.references) {