@yielded/auth-persistence-drizzle 0.1.0-beta.30 → 0.1.0-beta.32
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/README.md +13 -8
- package/dist/D1.d.mts +13 -4
- package/dist/D1.mjs +11 -1
- package/dist/D1.mjs.map +1 -1
- package/dist/Libsql.d.mts +4 -4
- package/dist/Libsql.mjs +1 -1
- package/dist/Mysql2.d.mts +4 -4
- package/dist/Pglite.d.mts +4 -4
- package/dist/Postgres.d.mts +4 -4
- package/dist/SqliteBun.d.mts +4 -4
- package/dist/SqliteBun.mjs +1 -1
- package/dist/SqliteDo.d.mts +4 -4
- package/dist/SqliteNode.d.mts +4 -4
- package/dist/SqliteNode.mjs +1 -1
- package/dist/SqliteWasm.d.mts +4 -4
- package/dist/SqliteWasm.mjs +1 -1
- package/dist/drizzle/oauth-target.mjs +5 -1
- package/dist/drizzle/oauth-target.mjs.map +1 -1
- package/dist/drizzle/passkey/target.d.mts +1 -1
- package/dist/drizzle/passkey/target.mjs.map +1 -1
- package/dist/drizzle/session-drivers.mjs +6 -5
- package/dist/drizzle/session-drivers.mjs.map +1 -1
- package/dist/drizzle/storage-validation.mjs +2 -1
- package/dist/drizzle/storage-validation.mjs.map +1 -1
- package/dist/internal/drizzle-d1-migrations.d.mts +14 -0
- package/dist/internal/drizzle-d1-migrations.mjs +83 -0
- package/dist/internal/drizzle-d1-migrations.mjs.map +1 -0
- package/dist/internal/drizzle-sqlite.mjs +7 -2
- package/dist/internal/drizzle-sqlite.mjs.map +1 -1
- package/package.json +1 -1
- package/src/D1.ts +12 -0
- package/src/drizzle/oauth-target.ts +5 -1
- package/src/drizzle/passkey/target.ts +5 -1
- package/src/drizzle/session-drivers.ts +16 -5
- package/src/drizzle/storage-validation.ts +2 -0
- package/src/internal/drizzle-d1-migrations.ts +150 -0
- package/src/internal/drizzle-sqlite.ts +14 -2
|
@@ -85,7 +85,11 @@ interface PasskeyMapped<M> {
|
|
|
85
85
|
readonly provide: <A, E, R>(
|
|
86
86
|
effect: Effect.Effect<A, E, R>,
|
|
87
87
|
) => Effect.Effect<A, E, Exclude<Exclude<R, SqlClient.SqlClient>, SqlBatchCommit>>;
|
|
88
|
-
readonly base: () => Effect.Effect<
|
|
88
|
+
readonly base: () => Effect.Effect<
|
|
89
|
+
NativePasskeyServices,
|
|
90
|
+
PasskeyUnavailable,
|
|
91
|
+
Crypto.Crypto | LifecycleHooks
|
|
92
|
+
>;
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
export const makePasskeyMapped = Effect.fnUntraced(function* <M, R>(
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
makeNativeSessionCleanupServices,
|
|
13
13
|
} from "@yielded/auth-persistence/Adapter";
|
|
14
14
|
import type { LifecycleHooks } from "@yielded/auth/Hooks";
|
|
15
|
+
import { reportAuthDiagnostic } from "@yielded/auth/Persistence";
|
|
15
16
|
import {
|
|
16
17
|
AuthenticationAuthority,
|
|
17
18
|
SessionUnavailable,
|
|
@@ -63,7 +64,9 @@ export const makeSessionTarget = <DatabaseId, D extends NativeDriverDatabase>(
|
|
|
63
64
|
configuration.mode === "batch" &&
|
|
64
65
|
(mapping.d1?.primary !== true || mapping.subject.requirementColumns === undefined)
|
|
65
66
|
)
|
|
66
|
-
return yield*
|
|
67
|
+
return yield* reportAuthDiagnostic("persistence-validation", "configuration").pipe(
|
|
68
|
+
Effect.andThen(unavailable()),
|
|
69
|
+
);
|
|
67
70
|
const target = yield* nativeTarget(configuration);
|
|
68
71
|
|
|
69
72
|
return yield* target.provide(
|
|
@@ -171,7 +174,9 @@ export const makeSessionTarget = <DatabaseId, D extends NativeDriverDatabase>(
|
|
|
171
174
|
configuration.mode === "batch" &&
|
|
172
175
|
(mapping.d1?.primary !== true || mapping.subject.requirementColumns === undefined)
|
|
173
176
|
)
|
|
174
|
-
return yield*
|
|
177
|
+
return yield* reportAuthDiagnostic("persistence-validation", "configuration").pipe(
|
|
178
|
+
Effect.andThen(unavailable()),
|
|
179
|
+
);
|
|
175
180
|
const target = yield* nativeTarget(configuration);
|
|
176
181
|
|
|
177
182
|
return yield* target.provide(makeNativePendingAuthenticationServices(target.tables, mapping));
|
|
@@ -275,7 +280,9 @@ export const makeSessionTarget = <DatabaseId, D extends NativeDriverDatabase>(
|
|
|
275
280
|
configuration.mode === "batch" &&
|
|
276
281
|
(mapping.d1?.primary !== true || mapping.subject.requirementColumns === undefined)
|
|
277
282
|
)
|
|
278
|
-
return yield*
|
|
283
|
+
return yield* reportAuthDiagnostic("persistence-validation", "configuration").pipe(
|
|
284
|
+
Effect.andThen(unavailable()),
|
|
285
|
+
);
|
|
279
286
|
const target = yield* nativeTarget(configuration);
|
|
280
287
|
|
|
281
288
|
return yield* target.provide(makeNativeStatefulSessionServices(target.tables, mapping));
|
|
@@ -397,7 +404,9 @@ export const makeSessionTarget = <DatabaseId, D extends NativeDriverDatabase>(
|
|
|
397
404
|
Effect.gen(function* () {
|
|
398
405
|
yield* validateDrizzleStorage(mapping).pipe(Effect.mapError(unavailable));
|
|
399
406
|
if (configuration.mode === "batch" && mapping.d1?.primary !== true)
|
|
400
|
-
return yield*
|
|
407
|
+
return yield* reportAuthDiagnostic("persistence-validation", "configuration").pipe(
|
|
408
|
+
Effect.andThen(unavailable()),
|
|
409
|
+
);
|
|
401
410
|
const target = yield* nativeTarget(configuration);
|
|
402
411
|
|
|
403
412
|
return yield* target.provide(makeNativeSignedSessionValidityServices(target.tables, mapping));
|
|
@@ -498,7 +507,9 @@ export const makeSessionTarget = <DatabaseId, D extends NativeDriverDatabase>(
|
|
|
498
507
|
configuration.mode === "batch" &&
|
|
499
508
|
(mapping.d1?.primary !== true || mapping.subject.requirementColumns === undefined)
|
|
500
509
|
)
|
|
501
|
-
return yield*
|
|
510
|
+
return yield* reportAuthDiagnostic("persistence-validation", "configuration").pipe(
|
|
511
|
+
Effect.andThen(unavailable()),
|
|
512
|
+
);
|
|
502
513
|
const target = yield* nativeTarget(configuration);
|
|
503
514
|
|
|
504
515
|
return yield* target.provide(makeNativeSessionStepUpServices(target.tables, mapping));
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type StorageValidation,
|
|
5
5
|
storageKeyPlans,
|
|
6
6
|
} from "@yielded/auth-persistence/Adapter";
|
|
7
|
+
import { reportAuthDiagnostic } from "@yielded/auth/Persistence";
|
|
7
8
|
import { getTableColumns, getTableName, is, Table } from "drizzle-orm";
|
|
8
9
|
import { getTableConfig as getMysqlTableConfig, MySqlTable } from "drizzle-orm/mysql-core";
|
|
9
10
|
import { getTableConfig as getPgTableConfig, PgTable } from "drizzle-orm/pg-core";
|
|
@@ -83,4 +84,5 @@ export const validateDrizzleStorage = Effect.fnUntraced(
|
|
|
83
84
|
}),
|
|
84
85
|
),
|
|
85
86
|
),
|
|
87
|
+
Effect.tapError(() => reportAuthDiagnostic("persistence-validation", "mapping")),
|
|
86
88
|
);
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { D1Client } from "@effect/sql-d1/D1Client";
|
|
2
|
+
import { PersistenceConfigurationError } from "@yielded/auth-persistence/Adapter";
|
|
3
|
+
import { sql } from "drizzle-orm";
|
|
4
|
+
import { drizzle } from "drizzle-orm/d1";
|
|
5
|
+
import { formatToMillis, getMigrationsToRun } from "drizzle-orm/migrator.utils";
|
|
6
|
+
import { Crypto, DateTime, Effect, Layer, Schema } from "effect";
|
|
7
|
+
|
|
8
|
+
const MigrationFiles = Schema.Record(Schema.String, Schema.NonEmptyString);
|
|
9
|
+
const MigrationName = Schema.String.check(Schema.isPattern(/^\d{14}_.+$/));
|
|
10
|
+
const JournalColumns = Schema.Array(Schema.Struct({ name: Schema.String }));
|
|
11
|
+
|
|
12
|
+
const configurationError = () =>
|
|
13
|
+
PersistenceConfigurationError.make({
|
|
14
|
+
reason: "Could not apply D1 migrations; check the bundled SQL and database binding",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const JournalRows = Schema.Array(
|
|
18
|
+
Schema.Struct({
|
|
19
|
+
id: Schema.Int,
|
|
20
|
+
hash: Schema.String,
|
|
21
|
+
created_at: Schema.String,
|
|
22
|
+
name: Schema.NullOr(Schema.String),
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
/** Drizzle's D1 migration journal and selection, with bundled SQL in place of
|
|
27
|
+
* filesystem loading. Pending statements and journal entries share one D1 batch.
|
|
28
|
+
* Applications own when migrations run; do not run competing migration owners.
|
|
29
|
+
*/
|
|
30
|
+
export const d1MigrationsLayer = (options: {
|
|
31
|
+
readonly migrations: Readonly<Record<string, string>>;
|
|
32
|
+
readonly migrationsTable?: string;
|
|
33
|
+
}) =>
|
|
34
|
+
Layer.effectDiscard(
|
|
35
|
+
Effect.gen(function* () {
|
|
36
|
+
const files = yield* Schema.decodeEffect(MigrationFiles)(options.migrations);
|
|
37
|
+
const client = yield* D1Client;
|
|
38
|
+
const crypto = yield* Crypto.Crypto;
|
|
39
|
+
const appliedAt = DateTime.formatIso(yield* DateTime.now);
|
|
40
|
+
|
|
41
|
+
const migrations = yield* Effect.forEach(
|
|
42
|
+
Object.entries(files).sort(([left], [right]) => left.localeCompare(right)),
|
|
43
|
+
Effect.fnUntraced(function* ([name, contents]) {
|
|
44
|
+
yield* Schema.decodeEffect(MigrationName)(name);
|
|
45
|
+
|
|
46
|
+
const folderMillis = yield* Schema.decodeEffect(Schema.Int)(
|
|
47
|
+
formatToMillis(name.slice(0, 14)),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const digest = yield* crypto.digest("SHA-256", new TextEncoder().encode(contents));
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
name,
|
|
54
|
+
folderMillis,
|
|
55
|
+
hash: Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join(""),
|
|
56
|
+
sql: contents.split("--> statement-breakpoint"),
|
|
57
|
+
bps: true,
|
|
58
|
+
};
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
yield* Effect.tryPromise({
|
|
63
|
+
try: async () => {
|
|
64
|
+
const db = drizzle(client.config.db);
|
|
65
|
+
const name = options.migrationsTable ?? "__drizzle_migrations";
|
|
66
|
+
const table = sql.identifier(name);
|
|
67
|
+
|
|
68
|
+
const columns = Schema.decodeUnknownSync(JournalColumns)(
|
|
69
|
+
await db.all(sql`select name from pragma_table_info(${name})`),
|
|
70
|
+
).map((column) => column.name);
|
|
71
|
+
|
|
72
|
+
const newDb = columns.length === 0;
|
|
73
|
+
const legacy = !newDb && !columns.includes("name");
|
|
74
|
+
|
|
75
|
+
if (
|
|
76
|
+
!newDb &&
|
|
77
|
+
(!["id", "hash", "created_at"].every((column) => columns.includes(column)) ||
|
|
78
|
+
legacy === columns.includes("applied_at"))
|
|
79
|
+
)
|
|
80
|
+
throw configurationError();
|
|
81
|
+
|
|
82
|
+
// Original Drizzle SQLite journals can have NULL serial IDs. Use the
|
|
83
|
+
// physical rowid to backfill each retained entry without changing it.
|
|
84
|
+
const journal = newDb
|
|
85
|
+
? []
|
|
86
|
+
: Schema.decodeUnknownSync(JournalRows)(
|
|
87
|
+
await db.all(sql`select rowid as id, hash, cast(created_at as text) as created_at,
|
|
88
|
+
${legacy ? sql`null` : sql`name`} as name from ${table} order by rowid`),
|
|
89
|
+
).map((entry) => ({ ...entry }));
|
|
90
|
+
|
|
91
|
+
const journalStatements = newDb
|
|
92
|
+
? [
|
|
93
|
+
db.run(sql`create table ${table} (
|
|
94
|
+
id integer primary key, hash text not null, created_at numeric,
|
|
95
|
+
name text, applied_at text
|
|
96
|
+
)`),
|
|
97
|
+
]
|
|
98
|
+
: [];
|
|
99
|
+
|
|
100
|
+
if (legacy) {
|
|
101
|
+
// Match retained entries using Drizzle's timestamp/hash convention,
|
|
102
|
+
// but stage the upgrade with pending SQL: D1 cannot run BEGIN.
|
|
103
|
+
journalStatements.push(
|
|
104
|
+
db.run(sql`alter table ${table} add column name text`),
|
|
105
|
+
db.run(sql`alter table ${table} add column applied_at text`),
|
|
106
|
+
);
|
|
107
|
+
for (const entry of journal) {
|
|
108
|
+
const millis = Math.floor(Number(entry.created_at) / 1000) * 1000;
|
|
109
|
+
|
|
110
|
+
const candidates = migrations.filter(
|
|
111
|
+
(migration) => migration.folderMillis === millis,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const matches =
|
|
115
|
+
candidates.length === 1
|
|
116
|
+
? candidates
|
|
117
|
+
: (candidates.length === 0 ? migrations : candidates).filter(
|
|
118
|
+
(migration) => migration.hash === entry.hash,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const match = matches[0];
|
|
122
|
+
|
|
123
|
+
if (matches.length !== 1 || match === undefined) throw configurationError();
|
|
124
|
+
entry.name = match.name;
|
|
125
|
+
journalStatements.push(
|
|
126
|
+
db.run(sql`update ${table} set name = ${match.name} where rowid = ${entry.id}`),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const statements = [
|
|
132
|
+
...journalStatements,
|
|
133
|
+
...getMigrationsToRun({
|
|
134
|
+
localMigrations: migrations,
|
|
135
|
+
dbMigrations: [...journal],
|
|
136
|
+
}).flatMap((migration) => [
|
|
137
|
+
...migration.sql.map((statement) => db.run(sql.raw(statement))),
|
|
138
|
+
db.run(sql`insert into ${table} (hash, created_at, name, applied_at)
|
|
139
|
+
values (${migration.hash}, ${migration.folderMillis}, ${migration.name}, ${appliedAt})`),
|
|
140
|
+
]),
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
const [first, ...rest] = statements;
|
|
144
|
+
|
|
145
|
+
if (first !== undefined) await db.batch([first, ...rest]);
|
|
146
|
+
},
|
|
147
|
+
catch: configurationError,
|
|
148
|
+
});
|
|
149
|
+
}).pipe(Effect.mapError(configurationError)),
|
|
150
|
+
);
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
uniqueIndex,
|
|
13
13
|
type SQLiteTable,
|
|
14
14
|
} from "drizzle-orm/sqlite-core";
|
|
15
|
-
import
|
|
15
|
+
import { Effect } from "effect";
|
|
16
16
|
import type { SqlClient } from "effect/sql/SqlClient";
|
|
17
17
|
|
|
18
18
|
import { nativeDatabase, type NativeDatabaseHandle } from "../drizzle/native-database";
|
|
@@ -71,10 +71,22 @@ const describe = (table: SQLiteTable): StorageTable => {
|
|
|
71
71
|
};
|
|
72
72
|
};
|
|
73
73
|
|
|
74
|
-
export const sqlitePersistence = <R>(
|
|
74
|
+
export const sqlitePersistence = <R>(
|
|
75
|
+
acquire: Effect.Effect<object, never, R | SqlClient>,
|
|
76
|
+
options?: { readonly batch: true },
|
|
77
|
+
) =>
|
|
75
78
|
createPersistence<SQLiteTable, R, NativeDatabaseHandle>({
|
|
76
79
|
nativeTables: (database) => makeDrizzleSqlTables(database.$client, database),
|
|
77
80
|
makeTable,
|
|
78
81
|
describe,
|
|
79
82
|
acquire: nativeDatabase(acquire),
|
|
83
|
+
...(options?.batch === true
|
|
84
|
+
? {
|
|
85
|
+
batch: (database: NativeDatabaseHandle) => ({
|
|
86
|
+
client: database.$client,
|
|
87
|
+
execute: (statements: Parameters<typeof database.$client.batch>[0]) =>
|
|
88
|
+
database.$client.batch(statements).pipe(Effect.asVoid),
|
|
89
|
+
}),
|
|
90
|
+
}
|
|
91
|
+
: {}),
|
|
80
92
|
});
|