Skip to content

Commit 9a58fde

Browse files
committed
surface the database type in the DatabaseClient options; when we create an instance without options, return the type derived from databases.json (or the name)
1 parent 6c2ec69 commit 9a58fde

File tree

7 files changed

+328
-3
lines changed

7 files changed

+328
-3
lines changed

src/databases/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,28 @@ export async function getQueryCachePath(
9696
const cacheName = `${await nameHash(databaseName)}-${await hash(strings, ...params)}.json`;
9797
return join(sourceDir, ".observable", "cache", cacheName);
9898
}
99+
100+
/**
101+
* Reads partial database configurations from the .observable/databases.json file.
102+
* For security reasons, no identifiers or passwords are returned.
103+
*/
104+
export async function getDatabaseConfigs(
105+
sourcePath: string
106+
): Promise<Map<string, Partial<DatabaseConfig>>> {
107+
const sourceDir = dirname(sourcePath);
108+
const configPath = join(sourceDir, ".observable", "databases.json");
109+
const databases = new Map<string, Partial<DatabaseConfig>>();
110+
111+
try {
112+
const configs = (await json(createReadStream(configPath, "utf-8"))) as Record<
113+
string,
114+
DatabaseConfig
115+
>;
116+
for (const [name, config] of Object.entries(configs)) {
117+
databases.set(name, {type: config.type});
118+
}
119+
} catch (error) {
120+
if (!isEnoent(error)) throw error;
121+
}
122+
return databases;
123+
}

src/javascript/__snapshots__/transpile.test.ts.snap

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,216 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3+
exports[`does not rewrite DatabaseClient with existing options 1`] = `
4+
{
5+
"autodisplay": true,
6+
"body": "(DatabaseClient) => {
7+
return (
8+
DatabaseClient("duckdb-base", {id: 1})
9+
)
10+
}",
11+
"inputs": [
12+
"DatabaseClient",
13+
],
14+
"output": undefined,
15+
"outputs": [],
16+
}
17+
`;
18+
19+
exports[`does not rewrite DatabaseClient with existing options 2`] = `
20+
{
21+
"autodisplay": true,
22+
"body": "(DatabaseClient) => {
23+
return (
24+
DatabaseClient("duckdb-base", {type: "postgres"})
25+
)
26+
}",
27+
"inputs": [
28+
"DatabaseClient",
29+
],
30+
"output": undefined,
31+
"outputs": [],
32+
}
33+
`;
34+
35+
exports[`rewrites DatabaseClient calls with type option 1`] = `
36+
{
37+
"autodisplay": true,
38+
"body": "(DatabaseClient) => {
39+
return (
40+
DatabaseClient("duckdb-base", {type: "duckdb"})
41+
)
42+
}",
43+
"inputs": [
44+
"DatabaseClient",
45+
],
46+
"output": undefined,
47+
"outputs": [],
48+
}
49+
`;
50+
51+
exports[`rewrites DatabaseClient calls with type option 2`] = `
52+
{
53+
"autodisplay": false,
54+
"body": "(DatabaseClient) => {
55+
const client = DatabaseClient("postgres-base", {type: "postgres"});
56+
return {client};
57+
}",
58+
"inputs": [
59+
"DatabaseClient",
60+
],
61+
"output": undefined,
62+
"outputs": [
63+
"client",
64+
],
65+
}
66+
`;
67+
68+
exports[`rewrites DatabaseClient calls with type option 3`] = `
69+
{
70+
"autodisplay": false,
71+
"body": "(DatabaseClient) => {
72+
const x = DatabaseClient("mydb-base", {type: "sqlite"});
73+
return {x};
74+
}",
75+
"inputs": [
76+
"DatabaseClient",
77+
],
78+
"output": undefined,
79+
"outputs": [
80+
"x",
81+
],
82+
}
83+
`;
84+
85+
exports[`rewrites DatabaseClient with default database names 1`] = `
86+
{
87+
"autodisplay": true,
88+
"body": "(DatabaseClient) => {
89+
return (
90+
DatabaseClient("postgres", {type: "postgres"})
91+
)
92+
}",
93+
"inputs": [
94+
"DatabaseClient",
95+
],
96+
"output": undefined,
97+
"outputs": [],
98+
}
99+
`;
100+
101+
exports[`rewrites DatabaseClient with default database names 2`] = `
102+
{
103+
"autodisplay": true,
104+
"body": "(DatabaseClient) => {
105+
return (
106+
DatabaseClient("duckdb", {type: "duckdb"})
107+
)
108+
}",
109+
"inputs": [
110+
"DatabaseClient",
111+
],
112+
"output": undefined,
113+
"outputs": [],
114+
}
115+
`;
116+
117+
exports[`rewrites DatabaseClient with default database names 3`] = `
118+
{
119+
"autodisplay": true,
120+
"body": "(DatabaseClient) => {
121+
return (
122+
DatabaseClient("sqlite", {type: "sqlite"})
123+
)
124+
}",
125+
"inputs": [
126+
"DatabaseClient",
127+
],
128+
"output": undefined,
129+
"outputs": [],
130+
}
131+
`;
132+
133+
exports[`rewrites DatabaseClient with file extension detection 1`] = `
134+
{
135+
"autodisplay": true,
136+
"body": "(DatabaseClient) => {
137+
return (
138+
DatabaseClient("data.duckdb", {type: "duckdb"})
139+
)
140+
}",
141+
"inputs": [
142+
"DatabaseClient",
143+
],
144+
"output": undefined,
145+
"outputs": [],
146+
}
147+
`;
148+
149+
exports[`rewrites DatabaseClient with file extension detection 2`] = `
150+
{
151+
"autodisplay": true,
152+
"body": "(DatabaseClient) => {
153+
return (
154+
DatabaseClient("mydata.db", {type: "sqlite"})
155+
)
156+
}",
157+
"inputs": [
158+
"DatabaseClient",
159+
],
160+
"output": undefined,
161+
"outputs": [],
162+
}
163+
`;
164+
165+
exports[`rewrites DatabaseClient with file extension detection 3`] = `
166+
{
167+
"autodisplay": true,
168+
"body": "(DatabaseClient) => {
169+
return (
170+
DatabaseClient("path/to/file.duckdb", {type: "duckdb"})
171+
)
172+
}",
173+
"inputs": [
174+
"DatabaseClient",
175+
],
176+
"output": undefined,
177+
"outputs": [],
178+
}
179+
`;
180+
181+
exports[`rewrites DatabaseClient with file extension detection 4`] = `
182+
{
183+
"autodisplay": true,
184+
"body": "(DatabaseClient) => {
185+
return (
186+
DatabaseClient("path/to/file.db", {type: "sqlite"})
187+
)
188+
}",
189+
"inputs": [
190+
"DatabaseClient",
191+
],
192+
"output": undefined,
193+
"outputs": [],
194+
}
195+
`;
196+
197+
exports[`transpiles DatabaseClient 1`] = `
198+
{
199+
"autodisplay": false,
200+
"body": "(DatabaseClient) => {
201+
const client = new DatabaseClient("name", {type: "duckdb"});
202+
return {client};
203+
}",
204+
"inputs": [
205+
"DatabaseClient",
206+
],
207+
"output": undefined,
208+
"outputs": [
209+
"client",
210+
],
211+
}
212+
`;
213+
3214
exports[`transpiles JavaScript expressions 1`] = `
4215
{
5216
"autodisplay": true,

src/javascript/databaseClient.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type {Node} from "acorn";
2+
import type {Sourcemap} from "./sourcemap.js";
3+
import {getStringLiteralValue, isStringLiteral} from "./strings.js";
4+
import {simple} from "./walk.js";
5+
import {DatabaseConfig} from "../databases/index.js";
6+
7+
export function rewriteDatabaseClient(
8+
output: Sourcemap,
9+
body: Node,
10+
databases: Map<string, Partial<DatabaseConfig>> = new Map()
11+
): void {
12+
simple(body, {
13+
CallExpression(node) {
14+
if (node.callee.type !== "Identifier" || node.callee.name !== "DatabaseClient") return;
15+
if (!isStringLiteral(node.arguments[0]))
16+
throw new Error("DatabaseClient name must be a string literal");
17+
18+
// if options are passed, don't change them
19+
if (node.arguments.length !== 1) return;
20+
21+
const name = getStringLiteralValue(node.arguments[0]);
22+
let type: string | undefined;
23+
if (databases.has(name)) {
24+
({type} = databases.get(name)!);
25+
} else {
26+
// see defaults in databases/index.ts ; @todo: unify?
27+
if (name === "postgres") type = "postgres";
28+
else if (name === "duckdb") type = "duckdb";
29+
else if (name === "sqlite") type = "sqlite";
30+
else if (/\.duckdb$/i.test(name)) type = "duckdb";
31+
else if (/\.db$/i.test(name)) type = "sqlite";
32+
else throw new Error(`database not found: ${name}`);
33+
}
34+
output.insertRight(node.arguments[0].end, `, {type: ${JSON.stringify(type)}}`);
35+
}
36+
});
37+
}

src/javascript/transpile.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,46 @@ it("transpiles node cells", () => {
6060
expect(transpile("process.stdout.write(`Node $\\{process.version}`);", "node")).toMatchSnapshot();
6161
expect(transpile("process.stdout.write(`Node \\$\\{process.version}`);", "node")).toMatchSnapshot();
6262
});
63+
64+
it("transpiles DatabaseClient", () => {
65+
expect(transpile('const client = new DatabaseClient("name", {type: "duckdb"});', "js")).toMatchSnapshot();
66+
});
67+
68+
it("rewrites DatabaseClient calls with type option", () => {
69+
const databases = new Map([["duckdb-base", {type: "duckdb"}], ["postgres-base", {type: "postgres"}], ["mydb-base", {type: "sqlite"}]]);
70+
expect(transpile('DatabaseClient("duckdb-base")', "js", {databases})).toMatchSnapshot();
71+
expect(transpile('const client = DatabaseClient("postgres-base");', "js", {databases})).toMatchSnapshot();
72+
expect(transpile('const x = DatabaseClient("mydb-base");', "js", {databases})).toMatchSnapshot();
73+
});
74+
75+
it("does not rewrite DatabaseClient with existing options", () => {
76+
const databases = new Map([["duckdb-base", {type: "duckdb"}]]);
77+
expect(transpile('DatabaseClient("duckdb-base", {id: 1})', "js", {databases})).toMatchSnapshot();
78+
expect(transpile('DatabaseClient("duckdb-base", {type: "postgres"})', "js", {databases})).toMatchSnapshot();
79+
});
80+
81+
it("throws error for unknown database name", () => {
82+
const databases = new Map([["duckdb-base", {type: "duckdb"}]]);
83+
expect(() => transpile('DatabaseClient("unknown")', "js", {databases})).toThrow("database not found: unknown");
84+
});
85+
86+
it("throws error for non-string-literal database name", () => {
87+
const databases = new Map([["duckdb-base", {type: "duckdb"}]]);
88+
expect(() => transpile("DatabaseClient(dbName)", "js", {databases})).toThrow("DatabaseClient name must be a string literal");
89+
expect(() => transpile("DatabaseClient(`template-${x}`)", "js", {databases})).toThrow("DatabaseClient name must be a string literal");
90+
});
91+
92+
it("rewrites DatabaseClient with default database names", () => {
93+
const databases = new Map();
94+
expect(transpile('DatabaseClient("postgres")', "js", {databases})).toMatchSnapshot();
95+
expect(transpile('DatabaseClient("duckdb")', "js", {databases})).toMatchSnapshot();
96+
expect(transpile('DatabaseClient("sqlite")', "js", {databases})).toMatchSnapshot();
97+
});
98+
99+
it("rewrites DatabaseClient with file extension detection", () => {
100+
const databases = new Map();
101+
expect(transpile('DatabaseClient("data.duckdb")', "js", {databases})).toMatchSnapshot();
102+
expect(transpile('DatabaseClient("mydata.db")', "js", {databases})).toMatchSnapshot();
103+
expect(transpile('DatabaseClient("path/to/file.duckdb")', "js", {databases})).toMatchSnapshot();
104+
expect(transpile('DatabaseClient("path/to/file.db")', "js", {databases})).toMatchSnapshot();
105+
});

src/javascript/transpile.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import {DatabaseConfig} from "../databases/index.js";
12
import type {Cell} from "../lib/notebook.js";
23
import {toCell} from "../lib/notebook.js";
4+
import {rewriteDatabaseClient} from "./databaseClient.js";
35
import {rewriteFileExpressions} from "./files.js";
46
import {hasImportDeclaration} from "./imports.js";
57
import {rewriteImportDeclarations, rewriteImportExpressions} from "./imports.js";
@@ -31,6 +33,8 @@ export type TranspileOptions = {
3133
resolveLocalImports?: boolean;
3234
/** If true, resolve file using import.meta.url (so Vite treats it as an asset). */
3335
resolveFiles?: boolean;
36+
/** If present, a map of database names to their types for DatabaseClient rewriting. */
37+
databases?: Map<string, Partial<DatabaseConfig>>;
3438
};
3539

3640
/** @deprecated */
@@ -86,6 +90,7 @@ export function transpileJavaScript(
8690
rewriteImportDeclarations(output, cell.body, inputs, options);
8791
rewriteImportExpressions(output, cell.body, options);
8892
if (options?.resolveFiles) rewriteFileExpressions(output, cell.body);
93+
if (options?.databases) rewriteDatabaseClient(output, cell.body, options.databases);
8994
if (cell.expression) output.insertLeft(0, `return (\n`);
9095
output.insertLeft(0, `${async ? "async " : ""}(${inputs}) => {\n`);
9196
if (outputs.length > 0) output.insertRight(input.length, `\nreturn {${outputs}};`);

src/runtime/stdlib/databaseClient.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ export interface QueryOptionsSpec {
3333
id?: number;
3434
/** if present, results are at least as fresh as the specified date */
3535
since?: Date | string | number;
36+
/** if present, the type of the database */
37+
type?: string;
3638
}
3739

3840
export interface QueryOptions extends QueryOptionsSpec {
@@ -49,10 +51,11 @@ export const DatabaseClient = (name: string, options?: QueryOptionsSpec): Databa
4951
return new DatabaseClientImpl(name, normalizeOptions(options));
5052
};
5153

52-
function normalizeOptions({id, since}: QueryOptionsSpec = {}): QueryOptions {
54+
function normalizeOptions({id, since, type}: QueryOptionsSpec = {}): QueryOptions {
5355
const options: QueryOptions = {};
5456
if (id !== undefined) options.id = id;
5557
if (since !== undefined) options.since = new Date(since);
58+
if (type !== undefined) options.type = type;
5659
return options;
5760
}
5861

0 commit comments

Comments
 (0)