@observablehq/notebook-kit 1.1.0-rc.1 → 1.1.0-rc.10
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/dist/package.json +8 -2
- package/dist/src/databases/duckdb.d.ts +1 -1
- package/dist/src/databases/duckdb.js +8 -3
- package/dist/src/databases/index.d.ts +13 -3
- package/dist/src/databases/index.js +41 -2
- package/dist/src/databases/sqlite.d.ts +2 -0
- package/dist/src/databases/sqlite.js +64 -0
- package/dist/src/javascript/imports/npm.js +2 -0
- package/dist/src/lib/hash.d.ts +2 -0
- package/dist/src/lib/hash.js +20 -0
- package/dist/src/lib/hash.test.d.ts +1 -0
- package/dist/src/lib/hash.test.js +28 -0
- package/dist/src/lib/sluggify.d.ts +6 -0
- package/dist/src/lib/sluggify.js +22 -0
- package/dist/src/lib/sluggify.test.d.ts +1 -0
- package/dist/src/lib/sluggify.test.js +51 -0
- package/dist/src/runtime/index.d.ts +2 -2
- package/dist/src/runtime/stdlib/databaseClient.d.ts +3 -4
- package/dist/src/runtime/stdlib/databaseClient.js +10 -13
- package/dist/src/runtime/stdlib/duckdb.js +7 -4
- package/dist/src/runtime/stdlib/index.d.ts +2 -2
- package/dist/src/vite/config.js +4 -2
- package/dist/src/vite/observable.js +11 -27
- package/package.json +8 -2
package/dist/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/observablehq/notebook-kit.git"
|
|
7
7
|
},
|
|
8
|
-
"version": "1.1.0-rc.
|
|
8
|
+
"version": "1.1.0-rc.10",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "vitest",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"lint": "tsc --noEmit && eslint bin src types",
|
|
14
14
|
"notebooks": "tsx bin/notebooks.ts",
|
|
15
15
|
"download": "tsx bin/notebooks.ts download",
|
|
16
|
-
"docs:preview": "tsx --watch bin/notebooks.ts preview --root docs --template docs/observable.tmpl",
|
|
16
|
+
"docs:preview": "tsx --watch bin/notebooks.ts preview --base /notebook-kit/ --root docs --template docs/observable.tmpl",
|
|
17
17
|
"docs:build": "tsx bin/notebooks.ts build --root docs --template docs/observable.tmpl -- $(find docs -path 'docs/.observable' -prune -o -name '*.html' -print)"
|
|
18
18
|
},
|
|
19
19
|
"bin": {
|
|
@@ -65,8 +65,10 @@
|
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@duckdb/node-api": "^1.3.2-alpha.26",
|
|
67
67
|
"@eslint/js": "^9.29.0",
|
|
68
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
68
69
|
"@types/jsdom": "^21.1.7",
|
|
69
70
|
"@types/markdown-it": "^14.1.2",
|
|
71
|
+
"better-sqlite3": "^12.2.0",
|
|
70
72
|
"eslint": "^9.29.0",
|
|
71
73
|
"globals": "^16.2.0",
|
|
72
74
|
"htl": "^0.3.1",
|
|
@@ -79,6 +81,7 @@
|
|
|
79
81
|
},
|
|
80
82
|
"peerDependencies": {
|
|
81
83
|
"@duckdb/node-api": "^1.3.2-alpha.26",
|
|
84
|
+
"better-sqlite3": "^12.2.0",
|
|
82
85
|
"postgres": "^3.4.7",
|
|
83
86
|
"snowflake-sdk": "^2.1.3"
|
|
84
87
|
},
|
|
@@ -86,6 +89,9 @@
|
|
|
86
89
|
"@duckdb/node-api": {
|
|
87
90
|
"optional": true
|
|
88
91
|
},
|
|
92
|
+
"better-sqlite3": {
|
|
93
|
+
"optional": true
|
|
94
|
+
},
|
|
89
95
|
"postgres": {
|
|
90
96
|
"optional": true
|
|
91
97
|
},
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { DatabaseContext, DuckDBConfig, QueryTemplateFunction } from "./index.js";
|
|
2
|
-
export default function duckdb(
|
|
2
|
+
export default function duckdb({ path, options }: DuckDBConfig, context: DatabaseContext): QueryTemplateFunction;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import { DuckDBConnection } from "@duckdb/node-api";
|
|
1
|
+
import { DuckDBConnection, DuckDBInstance } from "@duckdb/node-api";
|
|
2
2
|
import { BIGINT, BIT, BLOB, BOOLEAN, DATE, DOUBLE, FLOAT, HUGEINT, INTEGER, INTERVAL, SMALLINT, TIME, TIMESTAMP, TIMESTAMP_MS, TIMESTAMP_NS, TIMESTAMP_S, TIMESTAMPTZ, TINYINT, UBIGINT, UHUGEINT, UINTEGER, USMALLINT, UTINYINT, UUID, VARCHAR, VARINT } from "@duckdb/node-api"; // prettier-ignore
|
|
3
|
-
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
export default function duckdb({ path, options }, context) {
|
|
5
|
+
if (path !== undefined)
|
|
6
|
+
path = join(context.cwd, path);
|
|
4
7
|
return async (strings, ...params) => {
|
|
5
|
-
const
|
|
8
|
+
const instance = await DuckDBInstance.create(path, options);
|
|
9
|
+
const connection = await DuckDBConnection.create(instance);
|
|
6
10
|
await connection.run(`SET file_search_path=$0`, [context.cwd]);
|
|
7
11
|
const date = new Date();
|
|
8
12
|
let result;
|
|
@@ -13,6 +17,7 @@ export default function duckdb(_options, context) {
|
|
|
13
17
|
}
|
|
14
18
|
finally {
|
|
15
19
|
connection.disconnectSync();
|
|
20
|
+
instance.closeSync();
|
|
16
21
|
}
|
|
17
22
|
return {
|
|
18
23
|
rows,
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import type { ColumnSchema, QueryParam } from "../runtime/index.js";
|
|
2
|
-
export
|
|
2
|
+
export { hash as getQueryHash, nameHash as getNameHash } from "../lib/hash.js";
|
|
3
|
+
export type DatabaseConfig = DuckDBConfig | SQLiteConfig | SnowflakeConfig | PostgresConfig;
|
|
3
4
|
export type DuckDBConfig = {
|
|
4
5
|
type: "duckdb";
|
|
6
|
+
path?: string;
|
|
7
|
+
options?: {
|
|
8
|
+
[key: string]: string;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
export type SQLiteConfig = {
|
|
12
|
+
type: "sqlite";
|
|
13
|
+
path?: string;
|
|
5
14
|
};
|
|
6
15
|
export type SnowflakeConfig = {
|
|
7
16
|
type: "snowflake";
|
|
@@ -25,12 +34,13 @@ export type PostgresConfig = {
|
|
|
25
34
|
export type DatabaseContext = {
|
|
26
35
|
cwd: string;
|
|
27
36
|
};
|
|
28
|
-
export type QueryTemplateFunction = (strings: string[], ...params: QueryParam[]) => Promise<SerializableQueryResult>;
|
|
37
|
+
export type QueryTemplateFunction = (strings: readonly string[], ...params: QueryParam[]) => Promise<SerializableQueryResult>;
|
|
29
38
|
export type SerializableQueryResult = {
|
|
30
39
|
rows: Record<string, unknown>[];
|
|
31
40
|
schema: ColumnSchema[];
|
|
32
41
|
duration: number;
|
|
33
42
|
date: Date;
|
|
34
43
|
};
|
|
44
|
+
export declare function getDatabaseConfig(sourcePath: string, databaseName: string): Promise<DatabaseConfig>;
|
|
35
45
|
export declare function getDatabase(config: DatabaseConfig, context: DatabaseContext): Promise<QueryTemplateFunction>;
|
|
36
|
-
export declare function
|
|
46
|
+
export declare function getQueryCachePath(sourcePath: string, databaseName: string, strings: readonly string[], ...params: unknown[]): Promise<string>;
|
|
@@ -1,7 +1,44 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { json } from "node:stream/consumers";
|
|
4
|
+
import { isEnoent } from "../lib/error.js";
|
|
5
|
+
import { hash as getQueryHash, nameHash as getNameHash } from "../lib/hash.js";
|
|
6
|
+
export { hash as getQueryHash, nameHash as getNameHash } from "../lib/hash.js";
|
|
7
|
+
export async function getDatabaseConfig(sourcePath, databaseName) {
|
|
8
|
+
const sourceDir = dirname(sourcePath);
|
|
9
|
+
const configPath = join(sourceDir, ".observable", "databases.json");
|
|
10
|
+
let config;
|
|
11
|
+
try {
|
|
12
|
+
const configStream = createReadStream(configPath, "utf-8");
|
|
13
|
+
const configs = (await json(configStream));
|
|
14
|
+
config = configs[databaseName];
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (!isEnoent(error))
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
if (config === undefined) {
|
|
21
|
+
if (databaseName === "postgres")
|
|
22
|
+
config = { type: "postgres" };
|
|
23
|
+
else if (databaseName === "duckdb")
|
|
24
|
+
config = { type: "duckdb" };
|
|
25
|
+
else if (databaseName === "sqlite")
|
|
26
|
+
config = { type: "sqlite" };
|
|
27
|
+
else if (/\.duckdb$/i.test(databaseName))
|
|
28
|
+
config = { type: "duckdb", path: databaseName };
|
|
29
|
+
else if (/\.db$/i.test(databaseName))
|
|
30
|
+
config = { type: "sqlite", path: databaseName }; // TODO disambiguate
|
|
31
|
+
else
|
|
32
|
+
throw new Error(`database not found: ${databaseName}`);
|
|
33
|
+
}
|
|
34
|
+
return config;
|
|
35
|
+
}
|
|
1
36
|
export async function getDatabase(config, context) {
|
|
2
37
|
switch (config.type) {
|
|
3
38
|
case "duckdb":
|
|
4
39
|
return (await import("./duckdb.js")).default(config, context);
|
|
40
|
+
case "sqlite":
|
|
41
|
+
return (await import("./sqlite.js")).default(config, context);
|
|
5
42
|
case "snowflake":
|
|
6
43
|
return (await import("./snowflake.js")).default(config);
|
|
7
44
|
case "postgres":
|
|
@@ -10,6 +47,8 @@ export async function getDatabase(config, context) {
|
|
|
10
47
|
throw new Error(`unsupported database type: ${config["type"]}`);
|
|
11
48
|
}
|
|
12
49
|
}
|
|
13
|
-
export function
|
|
14
|
-
|
|
50
|
+
export async function getQueryCachePath(sourcePath, databaseName, strings, ...params) {
|
|
51
|
+
const sourceDir = dirname(sourcePath);
|
|
52
|
+
const cacheName = `${await getNameHash(databaseName)}-${await getQueryHash(strings, ...params)}.json`;
|
|
53
|
+
return join(sourceDir, ".observable", "cache", cacheName);
|
|
15
54
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import Database from "better-sqlite3";
|
|
3
|
+
export default function sqlite({ path = ":memory:" }, context) {
|
|
4
|
+
if (path !== undefined)
|
|
5
|
+
path = join(context.cwd, path);
|
|
6
|
+
return async (strings, ...params) => {
|
|
7
|
+
const date = new Date();
|
|
8
|
+
const database = new Database(path);
|
|
9
|
+
try {
|
|
10
|
+
const statement = database.prepare(strings.join("?"));
|
|
11
|
+
const rows = statement.all(...params);
|
|
12
|
+
return {
|
|
13
|
+
rows,
|
|
14
|
+
schema: getStatementSchema(statement),
|
|
15
|
+
duration: Date.now() - +date,
|
|
16
|
+
date
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
database.close();
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function getStatementSchema(statement) {
|
|
25
|
+
return statement
|
|
26
|
+
.columns()
|
|
27
|
+
.map((column) => ({ name: column.name, type: getColumnType(column.type) }));
|
|
28
|
+
}
|
|
29
|
+
function getColumnType(type) {
|
|
30
|
+
switch (type) {
|
|
31
|
+
case "INT":
|
|
32
|
+
case "INTEGER":
|
|
33
|
+
case "TINYINT":
|
|
34
|
+
case "SMALLINT":
|
|
35
|
+
case "MEDIUMINT":
|
|
36
|
+
case "BIGINT":
|
|
37
|
+
case "UNSIGNED BIG INT":
|
|
38
|
+
case "INT2":
|
|
39
|
+
case "INT8":
|
|
40
|
+
return "integer";
|
|
41
|
+
case "TEXT":
|
|
42
|
+
case "CLOB":
|
|
43
|
+
return "string";
|
|
44
|
+
case "REAL":
|
|
45
|
+
case "DOUBLE":
|
|
46
|
+
case "DOUBLE PRECISION":
|
|
47
|
+
case "FLOAT":
|
|
48
|
+
case "NUMERIC":
|
|
49
|
+
return "number";
|
|
50
|
+
case "BLOB":
|
|
51
|
+
return "buffer";
|
|
52
|
+
case "DATE":
|
|
53
|
+
case "DATETIME":
|
|
54
|
+
return "string"; // TODO convert strings to Date instances in sql.js
|
|
55
|
+
case null:
|
|
56
|
+
return "other";
|
|
57
|
+
default:
|
|
58
|
+
return /^(?:(?:(?:VARYING|NATIVE) )?CHARACTER|(?:N|VAR|NVAR)CHAR)\(/.test(type)
|
|
59
|
+
? "string"
|
|
60
|
+
: /^(?:DECIMAL|NUMERIC)\(/.test(type)
|
|
61
|
+
? "number"
|
|
62
|
+
: "other";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -25,6 +25,8 @@ function getDefaultRange(name) {
|
|
|
25
25
|
switch (name) {
|
|
26
26
|
case "@duckdb/duckdb-wasm":
|
|
27
27
|
return "@1.29.0"; // https://github.com/duckdb/duckdb-wasm/issues/1994
|
|
28
|
+
case "apache-arrow":
|
|
29
|
+
return "@17.0.0"; // to match @duckdb/duckdb-wasm 1.29.0
|
|
28
30
|
default:
|
|
29
31
|
return "";
|
|
30
32
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { sluggify } from "./sluggify.js";
|
|
2
|
+
async function sha256(input) {
|
|
3
|
+
const encoded = new TextEncoder().encode(input);
|
|
4
|
+
const buffer = await crypto.subtle.digest("SHA-256", encoded);
|
|
5
|
+
return new Uint8Array(buffer).reduce((i, byte) => (i << 8n) | BigInt(byte), 0n);
|
|
6
|
+
}
|
|
7
|
+
function base36(int, length) {
|
|
8
|
+
return int.toString(36).padStart(length, "0").slice(0, length);
|
|
9
|
+
}
|
|
10
|
+
export async function hash(strings, ...params) {
|
|
11
|
+
return base36(await sha256(JSON.stringify([strings, ...params])), 16);
|
|
12
|
+
}
|
|
13
|
+
export async function nameHash(name) {
|
|
14
|
+
return /^[\w-]+$/.test(name)
|
|
15
|
+
? name
|
|
16
|
+
: `${sluggify(basename(name))}.${base36(await sha256(name), 8)}`;
|
|
17
|
+
}
|
|
18
|
+
function basename(name) {
|
|
19
|
+
return name.replace(/^.*\//, "");
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { assert, describe, test } from "vitest";
|
|
2
|
+
import { hash, nameHash } from "./hash.js";
|
|
3
|
+
describe("nameHash", () => {
|
|
4
|
+
test("returns a simple name as-is", async () => {
|
|
5
|
+
assert.strictEqual(await nameHash("foo"), "foo");
|
|
6
|
+
assert.strictEqual(await nameHash("foo-bar"), "foo-bar");
|
|
7
|
+
assert.strictEqual(await nameHash("foo-"), "foo-");
|
|
8
|
+
assert.strictEqual(await nameHash("-foo"), "-foo");
|
|
9
|
+
});
|
|
10
|
+
test("sluggifies and hashes names with special characters", async () => {
|
|
11
|
+
assert.strictEqual(await nameHash("foo.db"), "foo-db.2s9flvsm");
|
|
12
|
+
assert.strictEqual(await nameHash("./foo.db"), "foo-db.3ee6cmxd");
|
|
13
|
+
assert.strictEqual(await nameHash("data/foo.db"), "foo-db.61nrbwb0");
|
|
14
|
+
assert.strictEqual(await nameHash("bar/foo.db"), "foo-db.1jlqjad7");
|
|
15
|
+
assert.strictEqual(await nameHash("foo bar"), "foo-bar.69w36b7f");
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
describe("hash", () => {
|
|
19
|
+
test("returns the expected static hash", async () => {
|
|
20
|
+
assert.strictEqual(await hash `foo`, "1n7k0l3ilxvth9al");
|
|
21
|
+
assert.strictEqual(await hash `bar`, "39v7mmkf7sfehxh1");
|
|
22
|
+
assert.strictEqual(await hash ``, "gepym5nmvuej8503");
|
|
23
|
+
});
|
|
24
|
+
test("returns the expected dynamic hash", async () => {
|
|
25
|
+
assert.strictEqual(await hash `SELECT 1 + ${2}`, "64iqby4orqj5tgek");
|
|
26
|
+
assert.strictEqual(await hash `SELECT 1 + ${3}`, "1azi8mazfb39kln7");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function sluggify(string, { length = 50, fallback = "untitled", separator = "-" } = {}) {
|
|
2
|
+
const parts = string
|
|
3
|
+
.normalize("NFD")
|
|
4
|
+
.replace(/[\u0300-\u036f'‘’]/g, "")
|
|
5
|
+
.toLowerCase()
|
|
6
|
+
.split(/\W+/g)
|
|
7
|
+
.filter(nonempty);
|
|
8
|
+
let i = -1;
|
|
9
|
+
for (let l = 0, n = parts.length; ++i < n;) {
|
|
10
|
+
if ((l += parts[i].length) + i > length) {
|
|
11
|
+
parts[i] = parts[i].substring(0, length - l + parts[i].length - i);
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return (parts
|
|
16
|
+
.slice(0, i + 1)
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.join(separator) || fallback.slice(0, length));
|
|
19
|
+
}
|
|
20
|
+
function nonempty(string) {
|
|
21
|
+
return string.length > 0;
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { assert, test } from "vitest";
|
|
2
|
+
import { sluggify } from "./sluggify.js";
|
|
3
|
+
test("returns the default fallback for empty slugs", () => {
|
|
4
|
+
assert.strictEqual(sluggify(""), "untitled");
|
|
5
|
+
assert.strictEqual(sluggify(" "), "untitled");
|
|
6
|
+
assert.strictEqual(sluggify("---"), "untitled");
|
|
7
|
+
assert.strictEqual(sluggify("##)!@#(*"), "untitled");
|
|
8
|
+
});
|
|
9
|
+
test("returns the given fallback for empty slugs", () => {
|
|
10
|
+
assert.strictEqual(sluggify("", { fallback: "foo" }), "foo");
|
|
11
|
+
assert.strictEqual(sluggify(" ", { fallback: "foo" }), "foo");
|
|
12
|
+
assert.strictEqual(sluggify("---", { fallback: "foo" }), "foo");
|
|
13
|
+
assert.strictEqual(sluggify("##)!@#(*", { fallback: "foo" }), "foo");
|
|
14
|
+
});
|
|
15
|
+
test("lowercases", () => {
|
|
16
|
+
assert.strictEqual(sluggify("HELLO WORLD"), "hello-world");
|
|
17
|
+
assert.strictEqual(sluggify("HelLo WorlD"), "hello-world");
|
|
18
|
+
});
|
|
19
|
+
test("removes emoji", () => {
|
|
20
|
+
assert.strictEqual(sluggify("HELLO 😎"), "hello");
|
|
21
|
+
assert.strictEqual(sluggify("HELLO 😎 world"), "hello-world");
|
|
22
|
+
assert.strictEqual(sluggify("HELLO 💩 world"), "hello-world");
|
|
23
|
+
});
|
|
24
|
+
test("trims leading and trailing spaces", () => {
|
|
25
|
+
assert.strictEqual(sluggify(" hello world "), "hello-world");
|
|
26
|
+
});
|
|
27
|
+
test("collapses contiguous spaces", () => {
|
|
28
|
+
assert.strictEqual(sluggify(" hello world "), "hello-world");
|
|
29
|
+
});
|
|
30
|
+
test("removes punctuation", () => {
|
|
31
|
+
assert.strictEqual(sluggify("Hello, world!"), "hello-world");
|
|
32
|
+
assert.strictEqual(sluggify("Hello, 'world'!"), "hello-world");
|
|
33
|
+
assert.strictEqual(sluggify('Hello, "world"!'), "hello-world");
|
|
34
|
+
assert.strictEqual(sluggify("Hello, “world”!"), "hello-world");
|
|
35
|
+
assert.strictEqual(sluggify("Hello, ‘world’!"), "hello-world");
|
|
36
|
+
assert.strictEqual(sluggify("Hello, fo'c's'le!"), "hello-focsle");
|
|
37
|
+
assert.strictEqual(sluggify("Hello, fo’c’s’le!"), "hello-focsle");
|
|
38
|
+
});
|
|
39
|
+
test("removes diacritics and combiners", () => {
|
|
40
|
+
assert.strictEqual(sluggify("Héllö, wørld!"), "hello-w-rld");
|
|
41
|
+
assert.strictEqual(sluggify("z̷̢̡̟͍̺͛͆͐̀ą̸̻̰̪͈͒͝ͅl̸͇̘̓g̶̡͈͒̾̉̽̑̅ö̸̧̟́͆"), "zalgo");
|
|
42
|
+
});
|
|
43
|
+
test("allows up to 50 characters after stripping", () => {
|
|
44
|
+
assert.strictEqual(sluggify("‘A‘ohe pu‘u ki‘eki‘e ke ho ‘ā‘o ‘ia e pi‘i"), "aohe-puu-kiekie-ke-ho-ao-ia-e-pii");
|
|
45
|
+
assert.strictEqual(sluggify("0123456789012345678901234567890123456789012345678"), "0123456789012345678901234567890123456789012345678");
|
|
46
|
+
assert.strictEqual(sluggify("01234567890123456789012345678901234567890123456789"), "01234567890123456789012345678901234567890123456789");
|
|
47
|
+
assert.strictEqual(sluggify("012345678901234567890123456789012345678901234567890"), "01234567890123456789012345678901234567890123456789");
|
|
48
|
+
assert.strictEqual(sluggify("01234567890 1234567890 1234567890 1234567890 12345678"), "01234567890-1234567890-1234567890-1234567890-12345");
|
|
49
|
+
assert.strictEqual(sluggify("01234567890 1234567890 1234567890 1234567890 123456789"), "01234567890-1234567890-1234567890-1234567890-12345");
|
|
50
|
+
assert.strictEqual(sluggify("01234567890 1234567890 1234567890 1234567890 1234567890"), "01234567890-1234567890-1234567890-1234567890-12345");
|
|
51
|
+
});
|
|
@@ -58,12 +58,12 @@ export declare class NotebookRuntime {
|
|
|
58
58
|
width: () => AsyncGenerator<number, void, unknown>;
|
|
59
59
|
DatabaseClient: () => {
|
|
60
60
|
(name: string, options?: import("./stdlib/databaseClient.js").QueryOptionsSpec): import("./stdlib/databaseClient.js").DatabaseClient;
|
|
61
|
-
hash: (strings: string[], ...params: unknown[]) => Promise<string>;
|
|
62
61
|
revive: ({ rows, schema, date, ...meta }: import("../databases/index.js").SerializableQueryResult) => import("./stdlib/databaseClient.js").QueryResult;
|
|
63
62
|
prototype: {
|
|
64
63
|
readonly name: string;
|
|
65
64
|
readonly options: import("./stdlib/databaseClient.js").QueryOptions;
|
|
66
|
-
sql(strings: string[], ...params: import("./stdlib/databaseClient.js").QueryParam[]): Promise<import("./stdlib/databaseClient.js").QueryResult>;
|
|
65
|
+
sql(strings: readonly string[], ...params: import("./stdlib/databaseClient.js").QueryParam[]): Promise<import("./stdlib/databaseClient.js").QueryResult>;
|
|
66
|
+
cachePath(strings: readonly string[], ...params: import("./stdlib/databaseClient.js").QueryParam[]): Promise<string>;
|
|
67
67
|
};
|
|
68
68
|
};
|
|
69
69
|
FileAttachment: () => {
|
|
@@ -27,11 +27,10 @@ export interface QueryOptions extends QueryOptionsSpec {
|
|
|
27
27
|
export interface DatabaseClient {
|
|
28
28
|
readonly name: string;
|
|
29
29
|
readonly options: QueryOptions;
|
|
30
|
-
sql(strings: string[], ...params: QueryParam[]): Promise<QueryResult>;
|
|
30
|
+
sql(strings: readonly string[], ...params: QueryParam[]): Promise<QueryResult>;
|
|
31
31
|
}
|
|
32
32
|
export declare const DatabaseClient: {
|
|
33
33
|
(name: string, options?: QueryOptionsSpec): DatabaseClient;
|
|
34
|
-
hash: typeof hash;
|
|
35
34
|
revive: typeof revive;
|
|
36
35
|
prototype: DatabaseClientImpl;
|
|
37
36
|
};
|
|
@@ -39,8 +38,8 @@ declare class DatabaseClientImpl implements DatabaseClient {
|
|
|
39
38
|
readonly name: string;
|
|
40
39
|
readonly options: QueryOptions;
|
|
41
40
|
constructor(name: string, options: QueryOptions);
|
|
42
|
-
sql(strings: string[], ...params: QueryParam[]): Promise<QueryResult>;
|
|
41
|
+
sql(strings: readonly string[], ...params: QueryParam[]): Promise<QueryResult>;
|
|
42
|
+
cachePath(strings: readonly string[], ...params: QueryParam[]): Promise<string>;
|
|
43
43
|
}
|
|
44
|
-
declare function hash(strings: string[], ...params: unknown[]): Promise<string>;
|
|
45
44
|
declare function revive({ rows, schema, date, ...meta }: SerializableQueryResult): QueryResult;
|
|
46
45
|
export {};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
+
import { hash, nameHash } from "../../lib/hash.js";
|
|
1
2
|
export const DatabaseClient = (name, options) => {
|
|
2
|
-
if (!/^[\w-]+$/.test(name))
|
|
3
|
-
throw new Error(`invalid database: ${name}`);
|
|
4
3
|
return new DatabaseClientImpl(name, normalizeOptions(options));
|
|
5
4
|
};
|
|
6
5
|
function normalizeOptions({ id, since } = {}) {
|
|
@@ -31,19 +30,15 @@ class DatabaseClientImpl {
|
|
|
31
30
|
});
|
|
32
31
|
}
|
|
33
32
|
async sql(strings, ...params) {
|
|
34
|
-
const path =
|
|
33
|
+
const path = await this.cachePath(strings, ...params);
|
|
35
34
|
const response = await fetch(path);
|
|
36
35
|
if (!response.ok)
|
|
37
36
|
throw new Error(`failed to fetch: ${path}`);
|
|
38
37
|
return await response.json().then(revive);
|
|
39
38
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const buffer = await crypto.subtle.digest("SHA-256", encoded);
|
|
44
|
-
const int = new Uint8Array(buffer).reduce((i, byte) => (i << 8n) | BigInt(byte), 0n);
|
|
45
|
-
const length = 16;
|
|
46
|
-
return int.toString(36).padStart(length, "0").slice(0, length);
|
|
39
|
+
async cachePath(strings, ...params) {
|
|
40
|
+
return `.observable/cache/${await nameHash(this.name)}-${await hash(strings, ...params)}.json`;
|
|
41
|
+
}
|
|
47
42
|
}
|
|
48
43
|
function revive({ rows, schema, date, ...meta }) {
|
|
49
44
|
for (const column of schema) {
|
|
@@ -54,7 +49,7 @@ function revive({ rows, schema, date, ...meta }) {
|
|
|
54
49
|
const value = row[name];
|
|
55
50
|
if (value == null)
|
|
56
51
|
continue;
|
|
57
|
-
row[name] =
|
|
52
|
+
row[name] = Number(value); // TODO BigInt?
|
|
58
53
|
}
|
|
59
54
|
break;
|
|
60
55
|
}
|
|
@@ -64,7 +59,7 @@ function revive({ rows, schema, date, ...meta }) {
|
|
|
64
59
|
const value = row[name];
|
|
65
60
|
if (value == null)
|
|
66
61
|
continue;
|
|
67
|
-
row[name] =
|
|
62
|
+
row[name] = asDate(value);
|
|
68
63
|
}
|
|
69
64
|
break;
|
|
70
65
|
}
|
|
@@ -74,7 +69,9 @@ function revive({ rows, schema, date, ...meta }) {
|
|
|
74
69
|
date = new Date(date);
|
|
75
70
|
return Object.assign(rows, { schema, date }, meta);
|
|
76
71
|
}
|
|
77
|
-
|
|
72
|
+
function asDate(value) {
|
|
73
|
+
return new Date(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(?::\d{2})?$/.test(value) ? value + "Z" : value);
|
|
74
|
+
}
|
|
78
75
|
DatabaseClient.revive = revive;
|
|
79
76
|
DatabaseClient.prototype = DatabaseClientImpl.prototype; // instanceof
|
|
80
77
|
Object.defineProperty(DatabaseClientImpl, "name", { value: "DatabaseClient" }); // prevent mangling
|
|
@@ -29,12 +29,12 @@ import * as duckdb from "npm:@duckdb/duckdb-wasm";
|
|
|
29
29
|
// POSSIBILITY OF SUCH DAMAGE.
|
|
30
30
|
const bundles = {
|
|
31
31
|
mvp: {
|
|
32
|
-
mainModule: "https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm/dist/duckdb-mvp.wasm",
|
|
33
|
-
mainWorker: "https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm/dist/duckdb-browser-mvp.worker.js"
|
|
32
|
+
mainModule: "https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@1.29.0/dist/duckdb-mvp.wasm",
|
|
33
|
+
mainWorker: "https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@1.29.0/dist/duckdb-browser-mvp.worker.js"
|
|
34
34
|
},
|
|
35
35
|
eh: {
|
|
36
|
-
mainModule: "https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm/dist/duckdb-eh.wasm",
|
|
37
|
-
mainWorker: "https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm/dist/duckdb-browser-eh.worker.js"
|
|
36
|
+
mainModule: "https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@1.29.0/dist/duckdb-eh.wasm",
|
|
37
|
+
mainWorker: "https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@1.29.0/dist/duckdb-browser-eh.worker.js"
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
40
|
const bundle = duckdb.selectBundle(bundles);
|
|
@@ -150,6 +150,9 @@ export class DuckDBClient {
|
|
|
150
150
|
}
|
|
151
151
|
static async of(sources = {}, config = {}) {
|
|
152
152
|
const db = await createDuckDB();
|
|
153
|
+
if (config.query?.castDecimalToDouble === undefined) {
|
|
154
|
+
config = { ...config, query: { ...config.query, castDecimalToDouble: true } };
|
|
155
|
+
}
|
|
153
156
|
if (config.query?.castTimestampToDate === undefined) {
|
|
154
157
|
config = { ...config, query: { ...config.query, castTimestampToDate: true } };
|
|
155
158
|
}
|
|
@@ -48,12 +48,12 @@ export declare const library: {
|
|
|
48
48
|
width: () => AsyncGenerator<number, void, unknown>;
|
|
49
49
|
DatabaseClient: () => {
|
|
50
50
|
(name: string, options?: import("./databaseClient.js").QueryOptionsSpec): DatabaseClient;
|
|
51
|
-
hash: (strings: string[], ...params: unknown[]) => Promise<string>;
|
|
52
51
|
revive: ({ rows, schema, date, ...meta }: import("../../databases/index.js").SerializableQueryResult) => import("./databaseClient.js").QueryResult;
|
|
53
52
|
prototype: {
|
|
54
53
|
readonly name: string;
|
|
55
54
|
readonly options: import("./databaseClient.js").QueryOptions;
|
|
56
|
-
sql(strings: string[], ...params: import("./databaseClient.js").QueryParam[]): Promise<import("./databaseClient.js").QueryResult>;
|
|
55
|
+
sql(strings: readonly string[], ...params: import("./databaseClient.js").QueryParam[]): Promise<import("./databaseClient.js").QueryResult>;
|
|
56
|
+
cachePath(strings: readonly string[], ...params: import("./databaseClient.js").QueryParam[]): Promise<string>;
|
|
57
57
|
};
|
|
58
58
|
};
|
|
59
59
|
FileAttachment: () => {
|
package/dist/src/vite/config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dirname, resolve } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { resolveNpmImport } from "../javascript/imports/npm.js";
|
|
3
4
|
const __filename = fileURLToPath(import.meta.url);
|
|
4
5
|
const __dirname = dirname(__filename);
|
|
5
6
|
export function config() {
|
|
@@ -14,8 +15,9 @@ export function config() {
|
|
|
14
15
|
resolve: {
|
|
15
16
|
alias: [
|
|
16
17
|
{
|
|
17
|
-
find: /^npm
|
|
18
|
-
replacement: "
|
|
18
|
+
find: /^(npm:.*)$/,
|
|
19
|
+
replacement: "$1",
|
|
20
|
+
customResolver: (source) => ({ id: resolveNpmImport(source), external: true })
|
|
19
21
|
},
|
|
20
22
|
{
|
|
21
23
|
find: /^jsr:(.*)$/,
|
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import { json } from "node:stream/consumers";
|
|
4
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { relative } from "node:path/posix";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { JSDOM } from "jsdom";
|
|
7
|
-
import { getDatabase,
|
|
8
|
-
import { isEnoent } from "../lib/error.js";
|
|
7
|
+
import { getDatabase, getDatabaseConfig, getQueryCachePath } from "../databases/index.js";
|
|
9
8
|
import { deserialize } from "../lib/serialize.js";
|
|
10
9
|
import { Sourcemap } from "../javascript/sourcemap.js";
|
|
11
10
|
import { transpile } from "../javascript/transpile.js";
|
|
12
11
|
import { parseTemplate } from "../javascript/template.js";
|
|
13
12
|
import { collectAssets } from "../runtime/stdlib/assets.js";
|
|
14
|
-
import { DatabaseClient } from "../runtime/stdlib/databaseClient.js";
|
|
15
13
|
import { highlight } from "../runtime/stdlib/highlight.js";
|
|
16
14
|
import { MarkdownRenderer } from "../runtime/stdlib/md.js";
|
|
17
15
|
export function observable({ window = new JSDOM().window, parser = new window.DOMParser(), template = fileURLToPath(import.meta.resolve("../templates/default.html")), transformTemplate = (template) => template, transformNotebook = (notebook) => notebook } = {}) {
|
|
@@ -54,7 +52,7 @@ export function observable({ window = new JSDOM().window, parser = new window.DO
|
|
|
54
52
|
const template = parseTemplate(value);
|
|
55
53
|
if (!template.expressions.length && !cell.output)
|
|
56
54
|
statics.add(cell);
|
|
57
|
-
const content = md([stripExpressions(template, value)]);
|
|
55
|
+
const content = md([unescapeDollarBackslashCurly(stripExpressions(template, value))]);
|
|
58
56
|
const codes = content.querySelectorAll("code[class^=language-]");
|
|
59
57
|
await Promise.all(Array.from(codes, highlight));
|
|
60
58
|
div.appendChild(content);
|
|
@@ -69,30 +67,13 @@ export function observable({ window = new JSDOM().window, parser = new window.DO
|
|
|
69
67
|
const template = parseTemplate(value);
|
|
70
68
|
if (!template.expressions.length) {
|
|
71
69
|
const dir = dirname(context.filename);
|
|
72
|
-
const
|
|
73
|
-
const hash = await DatabaseClient.hash.call(null, [value]);
|
|
74
|
-
const cacheName = `${cell.database}-${hash}.json`;
|
|
75
|
-
const cachePath = join(cacheDir, cacheName);
|
|
70
|
+
const cachePath = await getQueryCachePath(context.filename, cell.database, [value]);
|
|
76
71
|
if (!existsSync(cachePath)) {
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
const configPath = join(dir, ".observable", "databases.json");
|
|
80
|
-
const configStream = createReadStream(configPath, "utf-8");
|
|
81
|
-
const configs = (await json(configStream));
|
|
82
|
-
config = configs[cell.database];
|
|
83
|
-
}
|
|
84
|
-
catch (error) {
|
|
85
|
-
if (!isEnoent(error))
|
|
86
|
-
throw error;
|
|
87
|
-
}
|
|
88
|
-
if (isDefaultDatabase(cell.database))
|
|
89
|
-
config ?? (config = { type: cell.database });
|
|
90
|
-
if (!config)
|
|
91
|
-
throw new Error(`database not found: ${cell.database}`);
|
|
72
|
+
const config = await getDatabaseConfig(context.filename, cell.database);
|
|
92
73
|
try {
|
|
93
74
|
const database = await getDatabase(config, { cwd: dir });
|
|
94
75
|
const results = await database.call(null, [value]);
|
|
95
|
-
await mkdir(
|
|
76
|
+
await mkdir(dirname(cachePath), { recursive: true });
|
|
96
77
|
await writeFile(cachePath, JSON.stringify(results));
|
|
97
78
|
}
|
|
98
79
|
catch (error) {
|
|
@@ -100,7 +81,7 @@ export function observable({ window = new JSDOM().window, parser = new window.DO
|
|
|
100
81
|
}
|
|
101
82
|
}
|
|
102
83
|
cell.mode = "js";
|
|
103
|
-
cell.value = `FileAttachment(${JSON.stringify(
|
|
84
|
+
cell.value = `FileAttachment(${JSON.stringify(relative(dir, cachePath))}).json().then(DatabaseClient.revive)${hidden ? "" : `.then(Inputs.table)${cell.output ? ".then(view)" : ""}`}`;
|
|
104
85
|
}
|
|
105
86
|
}
|
|
106
87
|
collectAssets(assets, div);
|
|
@@ -197,6 +178,9 @@ function stripExpressions(template, input) {
|
|
|
197
178
|
}
|
|
198
179
|
return String(source);
|
|
199
180
|
}
|
|
181
|
+
function unescapeDollarBackslashCurly(input) {
|
|
182
|
+
return input.replace(/(\$\\*)\\({)/g, "$1$2");
|
|
183
|
+
}
|
|
200
184
|
/** Returns true if the specified character is preceded by an equals sign, ignoring whitespace. */
|
|
201
185
|
function hasPrecedingEquals(input, index) {
|
|
202
186
|
let i = index - 1;
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/observablehq/notebook-kit.git"
|
|
7
7
|
},
|
|
8
|
-
"version": "1.1.0-rc.
|
|
8
|
+
"version": "1.1.0-rc.10",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "vitest",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"lint": "tsc --noEmit && eslint bin src types",
|
|
14
14
|
"notebooks": "tsx bin/notebooks.ts",
|
|
15
15
|
"download": "tsx bin/notebooks.ts download",
|
|
16
|
-
"docs:preview": "tsx --watch bin/notebooks.ts preview --root docs --template docs/observable.tmpl",
|
|
16
|
+
"docs:preview": "tsx --watch bin/notebooks.ts preview --base /notebook-kit/ --root docs --template docs/observable.tmpl",
|
|
17
17
|
"docs:build": "tsx bin/notebooks.ts build --root docs --template docs/observable.tmpl -- $(find docs -path 'docs/.observable' -prune -o -name '*.html' -print)"
|
|
18
18
|
},
|
|
19
19
|
"bin": {
|
|
@@ -65,8 +65,10 @@
|
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@duckdb/node-api": "^1.3.2-alpha.26",
|
|
67
67
|
"@eslint/js": "^9.29.0",
|
|
68
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
68
69
|
"@types/jsdom": "^21.1.7",
|
|
69
70
|
"@types/markdown-it": "^14.1.2",
|
|
71
|
+
"better-sqlite3": "^12.2.0",
|
|
70
72
|
"eslint": "^9.29.0",
|
|
71
73
|
"globals": "^16.2.0",
|
|
72
74
|
"htl": "^0.3.1",
|
|
@@ -79,6 +81,7 @@
|
|
|
79
81
|
},
|
|
80
82
|
"peerDependencies": {
|
|
81
83
|
"@duckdb/node-api": "^1.3.2-alpha.26",
|
|
84
|
+
"better-sqlite3": "^12.2.0",
|
|
82
85
|
"postgres": "^3.4.7",
|
|
83
86
|
"snowflake-sdk": "^2.1.3"
|
|
84
87
|
},
|
|
@@ -86,6 +89,9 @@
|
|
|
86
89
|
"@duckdb/node-api": {
|
|
87
90
|
"optional": true
|
|
88
91
|
},
|
|
92
|
+
"better-sqlite3": {
|
|
93
|
+
"optional": true
|
|
94
|
+
},
|
|
89
95
|
"postgres": {
|
|
90
96
|
"optional": true
|
|
91
97
|
},
|