Skip to content

Commit 5427451

Browse files
committed
chore: tighten EmDash core and adapter type safety
Made-with: Cursor
1 parent 5f448d1 commit 5427451

27 files changed

Lines changed: 199 additions & 138 deletions

File tree

packages/auth/src/adapters/kysely.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,8 @@ interface AllowedDomainTable {
9393
// ============================================================================
9494

9595
export function createKyselyAdapter<T extends AuthTables>(db: Kysely<T>): AuthAdapter {
96-
// Type cast to work with generic Kysely instance
97-
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- generic Kysely<T extends AuthTables> narrowed to concrete AuthTables for internal queries
98-
const kdb = db as unknown as Kysely<AuthTables>;
96+
// `Kysely` is structurally compatible at runtime with the subset this adapter reads/writes.
97+
const kdb = db as Kysely<AuthTables>;
9998

10099
return {
101100
// ========================================================================

packages/cloudflare/src/db/d1-introspector.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,25 @@
77
* This introspector queries tables individually instead.
88
*/
99

10-
import type { DatabaseIntrospector, DatabaseMetadata, SchemaMetadata, TableMetadata } from "kysely";
10+
import type { DatabaseIntrospector, DatabaseMetadata, Kysely, SchemaMetadata, TableMetadata } from "kysely";
1111
import { sql } from "kysely";
1212

1313
// Kysely's default migration table names
1414
const DEFAULT_MIGRATION_TABLE = "kysely_migration";
1515
const DEFAULT_MIGRATION_LOCK_TABLE = "kysely_migration_lock";
1616

17-
// Kysely's DatabaseIntrospector.createIntrospector receives Kysely<any>.
18-
// We must use `any` here to match Kysely's own interface contract —
19-
// it needs untyped schema access to query sqlite_master dynamically.
20-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
21-
type AnyKysely = any;
17+
type IntrospectorShape = Record<string, Record<string, unknown>>;
2218

2319
// Regex patterns for parsing CREATE TABLE statements
2420
const SPLIT_PARENS_PATTERN = /[(),]/;
2521
const WHITESPACE_PATTERN = /\s+/;
2622
const QUOTES_PATTERN = /["`]/g;
2723

28-
export class D1Introspector implements DatabaseIntrospector {
29-
readonly #db: AnyKysely;
24+
export class D1Introspector<DB extends object = IntrospectorShape> implements DatabaseIntrospector {
25+
readonly #db: Kysely<DB & IntrospectorShape>;
3026

31-
constructor(db: AnyKysely) {
32-
this.#db = db;
27+
constructor(db: Kysely<DB>) {
28+
this.#db = db as Kysely<DB & IntrospectorShape>;
3329
}
3430

3531
async getSchemas(): Promise<SchemaMetadata[]> {

packages/cloudflare/src/db/d1.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { env } from "cloudflare:workers";
1212
import type { DatabaseIntrospector, Dialect, Kysely } from "kysely";
1313
import { D1Dialect } from "kysely-d1";
1414

15+
import type { Database } from "emdash";
1516
import { D1Introspector } from "./d1-introspector.js";
1617

1718
/**
@@ -30,7 +31,7 @@ interface D1Config {
3031
* cross-join with pragma_table_info() that D1 doesn't allow.
3132
*/
3233
class EmDashD1Dialect extends D1Dialect {
33-
override createIntrospector(db: Kysely<any>): DatabaseIntrospector {
34+
override createIntrospector(db: Kysely<Database>): DatabaseIntrospector {
3435
return new D1Introspector(db);
3536
}
3637
}

packages/cloudflare/src/db/do-dialect.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
} from "kysely";
1717
import { SqliteAdapter, SqliteQueryCompiler } from "kysely";
1818

19+
import type { Database } from "emdash";
1920
import { D1Introspector } from "./d1-introspector.js";
2021
import type { QueryResult as DOQueryResult } from "./do-class.js";
2122

@@ -62,7 +63,7 @@ export class PreviewDODialect implements Dialect {
6263
return new SqliteQueryCompiler();
6364
}
6465

65-
createIntrospector(db: Kysely<any>): DatabaseIntrospector {
66+
createIntrospector(db: Kysely<Database>): DatabaseIntrospector {
6667
return new D1Introspector(db);
6768
}
6869
}

packages/cloudflare/src/db/do-preview.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { runWithContext } from "emdash/request-context";
2626
import { Kysely } from "kysely";
2727
import { ulid } from "ulidx";
2828

29+
import type { Database } from "emdash";
2930
import type { EmDashPreviewDB } from "./do-class.js";
3031
import { PreviewDODialect } from "./do-dialect.js";
3132
import type { PreviewDBStub } from "./do-dialect.js";
@@ -221,13 +222,12 @@ export function createPreviewMiddleware(config: PreviewMiddlewareConfig): Middle
221222
// --- 4. Create Kysely dialect pointing at the DO ---
222223
const getStub = (): PreviewDBStub => {
223224
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- RPC type limitation
224-
return stub as unknown as PreviewDBStub;
225+
return stub as PreviewDBStub;
225226
};
226227
const dialect = new PreviewDODialect({ getStub });
227228

228229
// --- 5. Create Kysely instance and override request-context DB ---
229-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
230-
const previewDb = new Kysely<any>({ dialect });
230+
const previewDb = new Kysely<Database>({ dialect });
231231

232232
return runWithContext(
233233
{

packages/cloudflare/src/db/do.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export function createDialect(config: PreviewDOConfig & { name: string }): Diale
4848
const getStub = (): PreviewDBStub => {
4949
const stub = namespace.get(id);
5050
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Rpc type limitation with unknown in return types
51-
return stub as unknown as PreviewDBStub;
51+
return stub as PreviewDBStub;
5252
};
5353

5454
return new PreviewDODialect({ getStub });

packages/cloudflare/src/db/playground-middleware.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { ulid } from "ulidx";
2020
// @ts-ignore - virtual module populated by EmDash integration at build time
2121
import virtualConfig from "virtual:emdash/config";
2222

23+
import type { Database } from "emdash";
2324
import type { EmDashPreviewDB } from "./do-class.js";
2425
import { PreviewDODialect } from "./do-dialect.js";
2526
import type { PreviewDBStub } from "./do-dialect.js";
@@ -80,7 +81,7 @@ function getStub(binding: string, token: string): PreviewDBStub {
8081
const doId = namespace.idFromName(token);
8182
const stub = namespace.get(doId);
8283
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- RPC type limitation
83-
return stub as unknown as PreviewDBStub;
84+
return stub as PreviewDBStub;
8485
}
8586

8687
/**
@@ -119,8 +120,7 @@ function getSessionCreatedAt(token: string): string {
119120
* Initialize a playground DO: run migrations, apply seed, create admin user.
120121
*/
121122
async function initializePlayground(
122-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
123-
db: Kysely<any>,
123+
db: Kysely<Database>,
124124
token: string,
125125
): Promise<void> {
126126
// Check if already initialized (persisted in the DO)
@@ -288,7 +288,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
288288
const stub = getStub(binding, token);
289289
const dialect = new PreviewDODialect({ getStub: () => stub });
290290
// eslint-disable-next-line @typescript-eslint/no-explicit-any
291-
const db = new Kysely<any>({ dialect });
291+
const db = new Kysely<Database>({ dialect });
292292

293293
try {
294294
await initializePlayground(db, token);
@@ -334,7 +334,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
334334
const stub = getStub(binding, token);
335335
const dialect = new PreviewDODialect({ getStub: () => stub });
336336
// eslint-disable-next-line @typescript-eslint/no-explicit-any
337-
const db = new Kysely<any>({ dialect });
337+
const db = new Kysely<Database>({ dialect });
338338

339339
// Ensure initialized
340340
if (!initializedSessions.has(token)) {

packages/cloudflare/src/plugins/vectorize-search.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
import type { PluginDefinition, PluginContext, RouteContext, ContentHookEvent } from "emdash";
4646
import { extractPlainText } from "emdash";
4747

48+
const ASTRO_LOCALS_SYMBOL = Symbol.for("astro.locals");
49+
4850
/** Safely extract a string from an unknown value */
4951
function toString(value: unknown): string {
5052
return typeof value === "string" ? value : "";
@@ -55,6 +57,27 @@ function isRecord(value: unknown): value is Record<string, unknown> {
5557
return value != null && typeof value === "object" && !Array.isArray(value);
5658
}
5759

60+
interface AstroRequestLocals {
61+
runtime?: {
62+
env?: CloudflareEnv;
63+
};
64+
}
65+
66+
interface PortableTextLikeBlock {
67+
_type: string;
68+
[key: string]: unknown;
69+
}
70+
71+
function isPortableTextLikeArray(value: unknown[]): value is PortableTextLikeBlock[] {
72+
return value.every(
73+
(item) =>
74+
item !== null &&
75+
typeof item === "object" &&
76+
"_type" in item &&
77+
typeof (item as { _type?: unknown })._type === "string",
78+
);
79+
}
80+
5881
/**
5982
* Vectorize Search Plugin Configuration
6083
*/
@@ -84,8 +107,7 @@ export interface VectorizeSearchConfig {
84107
function getCloudflareEnv(request: Request): CloudflareEnv | null {
85108
// Access runtime.env from Astro's Cloudflare adapter
86109
// This is available when running on Cloudflare Workers
87-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, typescript-eslint(no-unsafe-type-assertion) -- Astro locals accessed via internal symbol; no typed API available
88-
const locals = (request as any)[Symbol.for("astro.locals")];
110+
const locals = (request as { [ASTRO_LOCALS_SYMBOL]?: AstroRequestLocals })[ASTRO_LOCALS_SYMBOL];
89111
if (locals?.runtime?.env) {
90112
return locals.runtime.env;
91113
}
@@ -112,9 +134,7 @@ function extractSearchableText(content: Record<string, unknown>): string {
112134
const text = extractPlainText(value);
113135
if (text) parts.push(text);
114136
} else if (Array.isArray(value)) {
115-
// Assume Portable Text array
116-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, typescript-eslint(no-unsafe-type-assertion) -- Portable Text arrays are untyped at this point; extractPlainText handles validation
117-
const text = extractPlainText(value as any);
137+
const text = isPortableTextLikeArray(value) ? extractPlainText(value) : JSON.stringify(value);
118138
if (text) parts.push(text);
119139
}
120140
}

packages/cloudflare/tests/db/playground-dialect.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Kysely } from "kysely";
22
import { describe, it, expect } from "vitest";
3+
import type { Database } from "emdash";
34

45
import { PreviewDODialect } from "../../src/db/do-dialect.js";
56
import type { PreviewDBStub } from "../../src/db/do-dialect.js";
@@ -32,7 +33,7 @@ describe("playground dummy dialect", () => {
3233

3334
it("throws when a query is executed (no middleware ALS override)", async () => {
3435
const dialect = createTestDialect();
35-
const db = new Kysely<any>({ dialect });
36+
const db = new Kysely<Database>({ dialect });
3637

3738
await expect(
3839
db

packages/core/src/api/handlers/marketplace.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -241,10 +241,7 @@ export async function loadBundleFromR2(
241241
const parsed: unknown = JSON.parse(manifestText);
242242
const result = pluginManifestSchema.safeParse(parsed);
243243
if (!result.success) return null;
244-
// Elements are validated as unknown[] by Zod; cast to PluginManifest
245-
// for the Element[] type (Block Kit validation happens at render time).
246-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Zod types elements as unknown[]; Element type validated at render time
247-
const manifest = result.data as unknown as PluginManifest;
244+
const manifest = result.data;
248245

249246
// Try to load admin code (optional)
250247
let adminCode: string | undefined;

0 commit comments

Comments
 (0)