datasette-ts 0.0.1

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.
@@ -0,0 +1,289 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { createReadStream } from "node:fs";
4
+ import { mkdir, stat, writeFile } from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ import { createClient } from "@libsql/client";
8
+
9
+ export async function dumpSqliteForD1(options) {
10
+ const baseName =
11
+ options.outputName ?? path.basename(options.dbFile, path.extname(options.dbFile));
12
+ const outputPath = path.join(options.outputDir, `${baseName}.sql`);
13
+
14
+ await mkdir(options.outputDir, { recursive: true });
15
+ const rawDump = execFileSync("sqlite3", [options.dbFile, ".dump"], {
16
+ encoding: "utf8",
17
+ });
18
+ const cleaned = normalizeDump(rawDump);
19
+ await writeFile(outputPath, cleaned);
20
+
21
+ return outputPath;
22
+ }
23
+
24
+ export async function loadSchemaFromFile(dbFile) {
25
+ const db = createReadonlyClient(dbFile);
26
+ try {
27
+ return await introspectDatabase(db);
28
+ } finally {
29
+ db.close();
30
+ }
31
+ }
32
+
33
+ export async function loadInspectDataFromFile(dbFile, databaseName) {
34
+ const stats = await stat(dbFile);
35
+ const hash = await hashFile(dbFile);
36
+ const tables = await countTables(dbFile);
37
+ return {
38
+ [databaseName]: {
39
+ hash,
40
+ size: stats.size,
41
+ file: path.basename(dbFile),
42
+ tables,
43
+ },
44
+ };
45
+ }
46
+
47
+ async function introspectDatabase(db) {
48
+ const rows = await executeRows(
49
+ db,
50
+ "select name, type, sql from sqlite_master where type in ('table', 'view') order by name",
51
+ );
52
+ const tables = {};
53
+
54
+ for (const row of rows) {
55
+ const name = asString(row.name);
56
+ if (name.startsWith("sqlite_")) {
57
+ continue;
58
+ }
59
+ const type = asString(row.type);
60
+ const sql = asNullableString(row.sql);
61
+ const virtual = type === "table" && isVirtualTable(sql);
62
+ const infoType = type === "view" ? "view" : virtual ? "virtual" : "table";
63
+ const columns = await tableColumns(db, name);
64
+ const foreignKeys = await tableForeignKeys(db, name);
65
+ const primaryKeys = columns
66
+ .filter((column) => column.primaryKey)
67
+ .sort((a, b) => a.primaryKeyIndex - b.primaryKeyIndex)
68
+ .map((column) => column.name);
69
+ const isFts = isFtsTable(sql);
70
+
71
+ tables[name] = {
72
+ name,
73
+ type: infoType,
74
+ sql,
75
+ columns,
76
+ primaryKeys,
77
+ foreignKeys,
78
+ isFts,
79
+ };
80
+ }
81
+
82
+ return { tables };
83
+ }
84
+
85
+ async function tableColumns(db, tableName) {
86
+ const rows = await executeRows(db, `pragma table_info(${escapeIdentifier(tableName)})`);
87
+ return rows.map((row) => ({
88
+ name: asString(row.name),
89
+ type: asString(row.type),
90
+ notNull: asNumber(row.notnull) === 1,
91
+ defaultValue: asNullableString(row.dflt_value),
92
+ primaryKey: asNumber(row.pk) > 0,
93
+ primaryKeyIndex: asNumber(row.pk),
94
+ }));
95
+ }
96
+
97
+ async function tableForeignKeys(db, tableName) {
98
+ const rows = await executeRows(db, `pragma foreign_key_list(${escapeIdentifier(tableName)})`);
99
+ return rows.map((row) => ({
100
+ table: asString(row.table),
101
+ from: asString(row.from),
102
+ to: asString(row.to),
103
+ onUpdate: asString(row.on_update),
104
+ onDelete: asString(row.on_delete),
105
+ match: asString(row.match),
106
+ seq: asNumber(row.seq),
107
+ id: asNumber(row.id),
108
+ }));
109
+ }
110
+
111
+ function asString(value, fallback = "") {
112
+ if (typeof value === "string") {
113
+ return value;
114
+ }
115
+ if (value == null) {
116
+ return fallback;
117
+ }
118
+ return String(value);
119
+ }
120
+
121
+ function asNumber(value, fallback = 0) {
122
+ if (typeof value === "number") {
123
+ return value;
124
+ }
125
+ if (typeof value === "string" && value.trim() !== "") {
126
+ const parsed = Number(value);
127
+ return Number.isNaN(parsed) ? fallback : parsed;
128
+ }
129
+ return fallback;
130
+ }
131
+
132
+ function asNullableString(value) {
133
+ if (value == null) {
134
+ return null;
135
+ }
136
+ if (typeof value === "string") {
137
+ return value;
138
+ }
139
+ return String(value);
140
+ }
141
+
142
+ function isVirtualTable(sql) {
143
+ if (!sql) {
144
+ return false;
145
+ }
146
+ return /create\s+virtual\s+table/i.test(sql);
147
+ }
148
+
149
+ function isFtsTable(sql) {
150
+ if (!sql) {
151
+ return false;
152
+ }
153
+ return /using\s+fts/i.test(sql);
154
+ }
155
+
156
+ function escapeIdentifier(name) {
157
+ const escaped = String(name).replace(/"/g, '""');
158
+ return `"${escaped}"`;
159
+ }
160
+
161
+ function normalizeDump(rawDump) {
162
+ const lines = rawDump.split("\n");
163
+ const output = [];
164
+ const tablesInOrder = [];
165
+ const viewsInOrder = [];
166
+ for (const line of lines) {
167
+ const tableMatch = line.match(/^CREATE TABLE\s+("?[^"]+"?)/i);
168
+ if (tableMatch) {
169
+ tablesInOrder.push(tableMatch[1].replace(/\s*\($/, ""));
170
+ continue;
171
+ }
172
+ const viewMatch = line.match(/^CREATE VIEW\s+("?[^"]+"?)/i);
173
+ if (viewMatch) {
174
+ viewsInOrder.push(viewMatch[1].replace(/\s*\($/, ""));
175
+ }
176
+ }
177
+ for (const viewName of viewsInOrder.reverse()) {
178
+ output.push(`DROP VIEW IF EXISTS ${viewName};`);
179
+ }
180
+ for (const tableName of tablesInOrder.reverse()) {
181
+ output.push(`DROP TABLE IF EXISTS ${tableName};`);
182
+ }
183
+ for (const line of lines) {
184
+ if (line === "BEGIN TRANSACTION;" || line === "COMMIT;") {
185
+ continue;
186
+ }
187
+ if (line.startsWith("PRAGMA foreign_keys=")) {
188
+ continue;
189
+ }
190
+ const tableMatch = line.match(/^CREATE TABLE\s+("?[^"]+"?)/i);
191
+ if (tableMatch) {
192
+ output.push(line);
193
+ continue;
194
+ }
195
+ const viewMatch = line.match(/^CREATE VIEW\s+("?[^"]+"?)/i);
196
+ if (viewMatch) {
197
+ output.push(line);
198
+ continue;
199
+ }
200
+ output.push(line);
201
+ }
202
+ return output.join("\n");
203
+ }
204
+
205
+ async function hashFile(filePath) {
206
+ return new Promise((resolve, reject) => {
207
+ const hash = createHash("sha256");
208
+ const stream = createReadStream(filePath, { highWaterMark: 1024 * 1024 });
209
+ stream.on("data", (chunk) => hash.update(chunk));
210
+ stream.on("error", reject);
211
+ stream.on("end", () => resolve(hash.digest("hex")));
212
+ });
213
+ }
214
+
215
+ async function countTables(dbFile) {
216
+ const db = createReadonlyClient(dbFile);
217
+ try {
218
+ const rows = await executeRows(db, "select name from sqlite_master where type = 'table'");
219
+ const tables = {};
220
+ for (const row of rows) {
221
+ const tableName = asString(row.name);
222
+ try {
223
+ const countRows = await executeRows(
224
+ db,
225
+ `select count(*) as count from ${escapeIdentifier(tableName)}`,
226
+ );
227
+ const countRow = countRows[0];
228
+ const countValue = countRow?.count;
229
+ const count = typeof countValue === "number" ? countValue : Number(countValue ?? 0);
230
+ tables[tableName] = { count: Number.isNaN(count) ? 0 : count };
231
+ } catch {
232
+ tables[tableName] = { count: 0 };
233
+ }
234
+ }
235
+ return tables;
236
+ } finally {
237
+ db.close();
238
+ }
239
+ }
240
+
241
+ function createReadonlyClient(dbFile) {
242
+ const fileUrl = pathToFileURL(dbFile);
243
+ return createClient({ url: fileUrl.toString() });
244
+ }
245
+
246
+ async function executeRows(db, sql, args = []) {
247
+ const result = await db.execute({ sql, args });
248
+ return normalizeLibsqlRows(result);
249
+ }
250
+
251
+ function normalizeLibsqlRows(result) {
252
+ const rows = Array.isArray(result?.rows) ? result.rows : [];
253
+ if (!rows.length) {
254
+ return [];
255
+ }
256
+
257
+ const firstRow = rows[0];
258
+ if (firstRow && typeof firstRow === "object" && !Array.isArray(firstRow)) {
259
+ return rows;
260
+ }
261
+
262
+ const columns = Array.isArray(result?.columns)
263
+ ? result.columns.map((column) => {
264
+ if (typeof column === "string") {
265
+ return column;
266
+ }
267
+ if (column && typeof column === "object" && "name" in column) {
268
+ return String(column.name ?? "");
269
+ }
270
+ return "";
271
+ })
272
+ : [];
273
+
274
+ if (!columns.length) {
275
+ return [];
276
+ }
277
+
278
+ return rows.map((row) => {
279
+ const values = Array.isArray(row) ? row : [];
280
+ const mapped = {};
281
+ for (let index = 0; index < columns.length; index += 1) {
282
+ const column = columns[index];
283
+ if (column) {
284
+ mapped[column] = values[index];
285
+ }
286
+ }
287
+ return mapped;
288
+ });
289
+ }