@better-auth/kysely-adapter 1.7.1 → 1.7.3

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.
@@ -1,6 +1,36 @@
1
1
  import { n as DEFAULT_MIGRATION_TABLE, t as DEFAULT_MIGRATION_LOCK_TABLE } from "./kysely-migration-tables-DFfmKhq-.mjs";
2
2
  import { SqliteAdapter, SqliteQueryCompiler } from "kysely";
3
3
  //#region src/d1-sqlite-dialect.ts
4
+ function quoteSqliteStringLiteral(value) {
5
+ return `'${value.replaceAll("'", "''")}'`;
6
+ }
7
+ function createD1IndexIntrospector(database) {
8
+ return async (tableNames) => {
9
+ if (tableNames.length === 0) return [];
10
+ const indexes = (await database.batch(tableNames.map((tableName) => database.prepare(`PRAGMA index_list(${quoteSqliteStringLiteral(tableName)})`)))).flatMap((indexList, tablePosition) => {
11
+ const tableName = tableNames[tablePosition];
12
+ if (!tableName) return [];
13
+ return indexList.results.map((index) => ({
14
+ ...index,
15
+ tableName
16
+ }));
17
+ });
18
+ if (indexes.length === 0) return [];
19
+ const indexColumns = await database.batch(indexes.map((index) => database.prepare(`PRAGMA index_info(${quoteSqliteStringLiteral(index.name)})`)));
20
+ return indexes.map((index, indexPosition) => ({
21
+ columns: (indexColumns[indexPosition]?.results ?? []).map((column) => ({
22
+ fullLength: column.name !== null,
23
+ name: column.name,
24
+ position: column.seqno
25
+ })),
26
+ name: index.name,
27
+ partial: index.partial !== 0,
28
+ table: index.tableName,
29
+ unique: index.unique !== 0,
30
+ valid: true
31
+ }));
32
+ };
33
+ }
4
34
  var D1SqliteAdapter = class extends SqliteAdapter {};
5
35
  var D1SqliteDriver = class {
6
36
  #config;
@@ -111,4 +141,4 @@ var D1SqliteDialect = class {
111
141
  }
112
142
  };
113
143
  //#endregion
114
- export { D1SqliteDialect };
144
+ export { D1SqliteDialect, createD1IndexIntrospector };
package/dist/index.d.mts CHANGED
@@ -1,15 +1,46 @@
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
6
7
  type KyselyDatabaseType = "postgres" | "mysql" | "sqlite" | "mssql";
8
+ /**
9
+ * Metadata for a column that participates in a database index.
10
+ */
11
+ interface DatabaseIndexColumnMetadata {
12
+ /**
13
+ * Whether the index covers the complete column value rather than a prefix.
14
+ */
15
+ readonly fullLength: boolean;
16
+ readonly name: string | null;
17
+ readonly position: number;
18
+ }
19
+ /**
20
+ * Database-agnostic metadata for an index.
21
+ */
22
+ interface DatabaseIndexMetadata {
23
+ readonly columns: readonly DatabaseIndexColumnMetadata[];
24
+ readonly name: string;
25
+ readonly partial: boolean;
26
+ readonly table: string;
27
+ readonly unique: boolean;
28
+ /**
29
+ * Whether the database reports the index as complete and usable.
30
+ */
31
+ readonly valid: boolean;
32
+ }
33
+ /**
34
+ * Reads normalized index metadata for the provided database tables.
35
+ */
36
+ type DatabaseIndexIntrospector = (tableNames: readonly string[]) => Promise<readonly DatabaseIndexMetadata[]>;
7
37
  //#endregion
8
38
  //#region src/dialect.d.ts
9
39
  declare function getKyselyDatabaseType(db: BetterAuthOptions["database"]): KyselyDatabaseType | null;
10
40
  declare const createKyselyAdapter: (config: BetterAuthOptions) => Promise<{
11
41
  kysely: Kysely<any> | null;
12
42
  databaseType: KyselyDatabaseType | null;
43
+ introspectIndexes: DatabaseIndexIntrospector | undefined;
13
44
  transaction: boolean | undefined;
14
45
  }>;
15
46
  //#endregion
@@ -55,4 +86,24 @@ interface KyselyAdapterConfig {
55
86
  }
56
87
  declare const kyselyAdapter: (db: Kysely<any>, config?: KyselyAdapterConfig | undefined) => (options: BetterAuthOptions) => DBAdapter<BetterAuthOptions>;
57
88
  //#endregion
58
- export { 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
@@ -25,19 +26,23 @@ const createKyselyAdapter = async (config) => {
25
26
  if (!db) return {
26
27
  kysely: null,
27
28
  databaseType: null,
29
+ introspectIndexes: void 0,
28
30
  transaction: void 0
29
31
  };
30
32
  if ("db" in db) return {
31
33
  kysely: db.db,
32
34
  databaseType: db.type,
35
+ introspectIndexes: void 0,
33
36
  transaction: db.transaction
34
37
  };
35
38
  if ("dialect" in db) return {
36
39
  kysely: new Kysely({ dialect: db.dialect }),
37
40
  databaseType: db.type,
41
+ introspectIndexes: void 0,
38
42
  transaction: db.transaction
39
43
  };
40
44
  let dialect = void 0;
45
+ let introspectIndexes = void 0;
41
46
  let transaction = void 0;
42
47
  const databaseType = getKyselyDatabaseType(db);
43
48
  if ("createDriver" in db) dialect = db;
@@ -77,13 +82,15 @@ const createKyselyAdapter = async (config) => {
77
82
  }
78
83
  }
79
84
  if ("batch" in db && "exec" in db && "prepare" in db) {
80
- const { D1SqliteDialect } = await import("./d1-sqlite-dialect-BZmaVTp8.mjs");
85
+ const { createD1IndexIntrospector, D1SqliteDialect } = await import("./d1-sqlite-dialect-D4qp4-wW.mjs");
81
86
  dialect = new D1SqliteDialect({ database: db });
87
+ introspectIndexes = createD1IndexIntrospector(db);
82
88
  transaction = false;
83
89
  }
84
90
  return {
85
91
  kysely: dialect ? new Kysely({ dialect }) : null,
86
92
  databaseType,
93
+ introspectIndexes,
87
94
  transaction
88
95
  };
89
96
  };
@@ -135,6 +142,105 @@ function insensitiveNe(columnRef, value) {
135
142
  };
136
143
  }
137
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
138
244
  //#region src/kysely-adapter.ts
139
245
  const kyselyAdapter = (db, config) => {
140
246
  let lazyOptions = null;
@@ -583,8 +689,10 @@ const kyselyAdapter = (db, config) => {
583
689
  const adapter = createAdapterFactory(adapterOptions);
584
690
  return (options) => {
585
691
  lazyOptions = options;
586
- 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;
587
695
  };
588
696
  };
589
697
  //#endregion
590
- 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.1",
3
+ "version": "1.7.3",
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.1"
45
+ "@better-auth/core": "^1.7.3"
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.1"
60
+ "@better-auth/core": "1.7.3"
59
61
  },
60
62
  "scripts": {
61
63
  "build": "tsdown",