@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
|
-
|
|
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-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
60
|
+
"@better-auth/core": "1.7.3"
|
|
59
61
|
},
|
|
60
62
|
"scripts": {
|
|
61
63
|
"build": "tsdown",
|