@better-auth/kysely-adapter 1.7.2 → 1.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,5 +1,6 @@
1
- import { Kysely } from "kysely";
1
+ import { Kysely, TableMetadata } from "kysely";
2
2
  import { DBAdapter, DBAdapterDebugLogOption } from "@better-auth/core/db/adapter";
3
+ import { ExpectedSchema, IntrospectedTable } from "@better-auth/core/db/internal";
3
4
  import { BetterAuthOptions } from "@better-auth/core";
4
5
 
5
6
  //#region src/types.d.ts
@@ -85,4 +86,24 @@ interface KyselyAdapterConfig {
85
86
  }
86
87
  declare const kyselyAdapter: (db: Kysely<any>, config?: KyselyAdapterConfig | undefined) => (options: BetterAuthOptions) => DBAdapter<BetterAuthOptions>;
87
88
  //#endregion
88
- export { DatabaseIndexColumnMetadata, DatabaseIndexIntrospector, DatabaseIndexMetadata, KyselyDatabaseType, createKyselyAdapter, getKyselyDatabaseType, kyselyAdapter };
89
+ //#region src/schema-check.d.ts
90
+ /**
91
+ * Converts Kysely table metadata into the shape `diffSchema` compares.
92
+ */
93
+ declare function toIntrospectedTables(tables: readonly TableMetadata[]): IntrospectedTable[];
94
+ /**
95
+ * The expected schema in the identifiers the connection sends. A plugin that
96
+ * renames identifiers, such as `CamelCasePlugin`, does so in `transformQuery`,
97
+ * so compiling one select per table through the connection yields the names
98
+ * the database is asked for. Without such a plugin the schema is unchanged.
99
+ */
100
+ declare function toPhysicalSchema(db: Kysely<unknown>, expected: ExpectedSchema): ExpectedSchema;
101
+ /**
102
+ * The default schema for migration tooling. Let PostgreSQL resolve role
103
+ * names and privileges, retaining the legacy public fallback when none exists.
104
+ * Runtime validation uses the effective search path instead of this fallback.
105
+ */
106
+ declare function getPostgresSchema(db: Kysely<unknown>): Promise<string>;
107
+ declare function getMssqlSchema(db: Kysely<unknown>): Promise<string>;
108
+ //#endregion
109
+ export { DatabaseIndexColumnMetadata, DatabaseIndexIntrospector, DatabaseIndexMetadata, KyselyDatabaseType, createKyselyAdapter, getKyselyDatabaseType, getMssqlSchema, getPostgresSchema, kyselyAdapter, toIntrospectedTables, toPhysicalSchema };
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
- import { Kysely, MssqlDialect, MysqlDialect, PostgresDialect, SqliteDialect, sql } from "kysely";
1
+ import { ColumnNode, Kysely, MssqlDialect, MysqlDialect, PostgresDialect, ReferenceNode, SelectQueryNode, SqliteDialect, TableNode, sql } from "kysely";
2
2
  import { createAdapterFactory } from "@better-auth/core/db/adapter";
3
+ import { checksSchema, createSchemaCheck, diffSchema, getExpectedSchema, registerSchemaCheck } from "@better-auth/core/db/internal";
3
4
  import { logger } from "@better-auth/core/env";
4
5
  import { capitalizeFirstLetter } from "@better-auth/core/utils/string";
5
6
  //#region src/dialect.ts
@@ -141,6 +142,105 @@ function insensitiveNe(columnRef, value) {
141
142
  };
142
143
  }
143
144
  //#endregion
145
+ //#region src/schema-check.ts
146
+ /**
147
+ * Converts Kysely table metadata into the shape `diffSchema` compares.
148
+ */
149
+ function toIntrospectedTables(tables) {
150
+ return tables.map((table) => ({
151
+ name: table.name,
152
+ schema: table.schema,
153
+ columns: table.columns.map((column) => ({
154
+ name: column.name,
155
+ nullable: column.isNullable,
156
+ hasDefault: column.hasDefaultValue || column.isAutoIncrementing
157
+ }))
158
+ }));
159
+ }
160
+ /**
161
+ * The expected schema in the identifiers the connection sends. A plugin that
162
+ * renames identifiers, such as `CamelCasePlugin`, does so in `transformQuery`,
163
+ * so compiling one select per table through the connection yields the names
164
+ * the database is asked for. Without such a plugin the schema is unchanged.
165
+ */
166
+ function toPhysicalSchema(db, expected) {
167
+ const physical = {};
168
+ for (const [table, definition] of Object.entries(expected)) {
169
+ const entries = Object.entries(definition.fields);
170
+ const sent = sentIdentifiers(db, table, [definition.idColumn ?? "id", ...entries.map(([column]) => column)]);
171
+ const fields = {};
172
+ entries.forEach(([column, attribute], index) => {
173
+ fields[sent.columns[index + 1] ?? column] = attribute;
174
+ });
175
+ physical[sent.table] = {
176
+ ...definition,
177
+ fields,
178
+ schema: sent.schema ?? definition.schema,
179
+ ...sent.columns[0] !== "id" && { idColumn: sent.columns[0] }
180
+ };
181
+ }
182
+ return physical;
183
+ }
184
+ /**
185
+ * Compiles a select of `columns` from `table` and reads the identifiers back
186
+ * out of the transformed query, including the schema a plugin such as
187
+ * `WithSchemaPlugin` qualifies the table with. A node the query does not
188
+ * carry in the expected shape leaves its identifier undefined.
189
+ */
190
+ function sentIdentifiers(db, table, columns) {
191
+ const { query } = db.selectFrom(table).select(columns).compile();
192
+ if (!SelectQueryNode.is(query)) return {
193
+ table,
194
+ columns: []
195
+ };
196
+ const from = query.from?.froms[0];
197
+ const sentTable = from && TableNode.is(from) ? from.table : void 0;
198
+ return {
199
+ schema: sentTable?.schema?.name,
200
+ table: sentTable?.identifier.name ?? table,
201
+ columns: (query.selections ?? []).map(({ selection }) => ReferenceNode.is(selection) && ColumnNode.is(selection.column) ? selection.column.column.name : void 0)
202
+ };
203
+ }
204
+ /**
205
+ * The default schema for migration tooling. Let PostgreSQL resolve role
206
+ * names and privileges, retaining the legacy public fallback when none exists.
207
+ * Runtime validation uses the effective search path instead of this fallback.
208
+ */
209
+ async function getPostgresSchema(db) {
210
+ return (await sql`
211
+ SELECT pg_catalog.current_schema() AS schema
212
+ `.execute(db.withoutPlugins())).rows[0]?.schema ?? "public";
213
+ }
214
+ async function getMssqlSchema(db) {
215
+ return (await sql`
216
+ SELECT SCHEMA_NAME() AS "schemaName"
217
+ `.execute(db.withoutPlugins())).rows[0]?.schemaName || "dbo";
218
+ }
219
+ async function schemaSearchPath(db, dbType) {
220
+ if (dbType === "postgres") return (await sql`
221
+ SELECT pg_catalog.current_schemas(true)::text[] AS schemas
222
+ `.execute(db.withoutPlugins())).rows[0]?.schemas ?? [];
223
+ if (dbType === "mssql") return [await getMssqlSchema(db), "dbo"];
224
+ }
225
+ /**
226
+ * Compares the live database with the tables this configuration writes. Both
227
+ * sides are read in the identifiers the connection sends: a plugin that
228
+ * renames identifiers or qualifies them with a schema is applied to the
229
+ * expected side, and introspection reports what the database stores.
230
+ */
231
+ async function findSchemaProblems(db, dbType, expected) {
232
+ const physical = toPhysicalSchema(db, expected);
233
+ return db.connection().execute(async (connection) => {
234
+ const searchPath = await schemaSearchPath(connection, dbType);
235
+ const tables = toIntrospectedTables(await connection.introspection.getTables());
236
+ for (const [name, table] of Object.entries(physical)) {
237
+ if (table.schema !== void 0 || !searchPath) continue;
238
+ table.schema = searchPath.find((schema) => tables.some((candidate) => candidate.name === name && candidate.schema === schema)) ?? "";
239
+ }
240
+ return diffSchema(physical, tables);
241
+ });
242
+ }
243
+ //#endregion
144
244
  //#region src/kysely-adapter.ts
145
245
  const kyselyAdapter = (db, config) => {
146
246
  let lazyOptions = null;
@@ -589,8 +689,10 @@ const kyselyAdapter = (db, config) => {
589
689
  const adapter = createAdapterFactory(adapterOptions);
590
690
  return (options) => {
591
691
  lazyOptions = options;
592
- return adapter(options);
692
+ const instance = adapter(options);
693
+ if (checksSchema(options)) registerSchemaCheck(instance, createSchemaCheck(() => findSchemaProblems(db, config?.type, getExpectedSchema(options, { usePlural: config?.usePlural })), "database", options.database));
694
+ return instance;
593
695
  };
594
696
  };
595
697
  //#endregion
596
- export { createKyselyAdapter, getKyselyDatabaseType, kyselyAdapter };
698
+ export { createKyselyAdapter, getKyselyDatabaseType, getMssqlSchema, getPostgresSchema, kyselyAdapter, toIntrospectedTables, toPhysicalSchema };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/kysely-adapter",
3
- "version": "1.7.2",
3
+ "version": "1.7.4",
4
4
  "description": "Kysely adapter for Better Auth",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -42,7 +42,7 @@
42
42
  "peerDependencies": {
43
43
  "@better-auth/utils": "0.4.2",
44
44
  "kysely": "^0.28.17 || ^0.29.0",
45
- "@better-auth/core": "^1.7.2"
45
+ "@better-auth/core": "^1.7.4"
46
46
  },
47
47
  "peerDependenciesMeta": {
48
48
  "kysely": {
@@ -52,10 +52,12 @@
52
52
  "devDependencies": {
53
53
  "@better-auth/utils": "0.4.2",
54
54
  "@cloudflare/workers-types": "^4.20250121.0",
55
+ "@types/pg": "^8.20.0",
55
56
  "kysely": "^0.28.17 || ^0.29.0",
57
+ "pg": "^8.0.0",
56
58
  "tsdown": "0.21.10",
57
59
  "typescript": "^6.0.3",
58
- "@better-auth/core": "1.7.2"
60
+ "@better-auth/core": "1.7.4"
59
61
  },
60
62
  "scripts": {
61
63
  "build": "tsdown",