@lunora/d1 0.0.0 → 1.0.0-alpha.2
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/LICENSE.md +105 -0
- package/README.md +114 -9
- package/__assets__/package-og.svg +14 -0
- package/dist/dialect.d.mts +39 -0
- package/dist/dialect.d.ts +39 -0
- package/dist/dialect.mjs +35 -0
- package/dist/index.d.mts +339 -0
- package/dist/index.d.ts +339 -0
- package/dist/index.mjs +7 -0
- package/dist/packem_shared/D1Client-DA3flo1o.mjs +143 -0
- package/dist/packem_shared/MigrationRunner-BkEwQ-Ya.mjs +149 -0
- package/dist/packem_shared/createD1CtxDb-BMR8J0dT.mjs +14 -0
- package/dist/packem_shared/exportGlobalRows-BGCPm_nA.mjs +122 -0
- package/dist/packem_shared/facetGlobalColumn-C6u_WMIY.mjs +142 -0
- package/dist/packem_shared/sqliteDialect-DqYnHPuu.mjs +27 -0
- package/package.json +46 -17
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { decodeGlobalRow } from '@lunora/sql-store';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_BATCH_SIZE = 200;
|
|
4
|
+
const quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
5
|
+
const selectGlobalTables = (schema, requested) => {
|
|
6
|
+
const isGlobal = (table) => schema.tables[table]?.shardMode?.kind === "global";
|
|
7
|
+
if (requested && requested.length > 0) {
|
|
8
|
+
return requested.filter((name) => isGlobal(name));
|
|
9
|
+
}
|
|
10
|
+
return Object.keys(schema.tables).filter((name) => isGlobal(name));
|
|
11
|
+
};
|
|
12
|
+
const decodeRow = (schema, table, row) => {
|
|
13
|
+
const definition = schema.tables[table];
|
|
14
|
+
if (!definition) {
|
|
15
|
+
return { _creationTime: row["_creationTime"], _id: row["id"] };
|
|
16
|
+
}
|
|
17
|
+
return decodeGlobalRow(definition, row);
|
|
18
|
+
};
|
|
19
|
+
const exportGlobalRows = async function* (exec, schema, args) {
|
|
20
|
+
const tables = selectGlobalTables(schema, args.tables);
|
|
21
|
+
const batchSize = args.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
22
|
+
for (const table of tables) {
|
|
23
|
+
let offset = 0;
|
|
24
|
+
let hasMore = true;
|
|
25
|
+
while (hasMore) {
|
|
26
|
+
const rows = await exec.all(`SELECT * FROM ${quoteIdentifier(table)} LIMIT ? OFFSET ?`, [batchSize, offset]);
|
|
27
|
+
for (const row of rows) {
|
|
28
|
+
yield { doc: decodeRow(schema, table, row), table };
|
|
29
|
+
}
|
|
30
|
+
hasMore = rows.length === batchSize;
|
|
31
|
+
offset += rows.length;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const validateRow = (schema, table, document) => {
|
|
36
|
+
const definition = schema.tables[table];
|
|
37
|
+
if (!definition) {
|
|
38
|
+
return `unknown table: ${table}`;
|
|
39
|
+
}
|
|
40
|
+
for (const [field, validator] of Object.entries(definition.shape)) {
|
|
41
|
+
const candidate = document[field];
|
|
42
|
+
const optional = validator.kind === "optional";
|
|
43
|
+
if (candidate === void 0 && optional) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const parser = validator.parse;
|
|
47
|
+
if (typeof parser !== "function") {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
parser(candidate);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
54
|
+
return `field "${field}": ${message}`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return void 0;
|
|
58
|
+
};
|
|
59
|
+
const explicitIdConflicts = async (writer, exec, table, explicitId) => {
|
|
60
|
+
try {
|
|
61
|
+
if (exec) {
|
|
62
|
+
const probe = await exec.all(`SELECT 1 AS hit FROM ${quoteIdentifier(table)} WHERE "id" = ? LIMIT 1`, [explicitId]);
|
|
63
|
+
return probe.length > 0;
|
|
64
|
+
}
|
|
65
|
+
const existing = await writer.get(explicitId);
|
|
66
|
+
return existing !== null;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const importOneRow = async (writer, schema, args, row, line) => {
|
|
72
|
+
const { doc, table } = row;
|
|
73
|
+
if (schema.tables[table]?.shardMode?.kind !== "global") {
|
|
74
|
+
return { kind: "skip" };
|
|
75
|
+
}
|
|
76
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
|
|
77
|
+
return { error: { code: "BAD_ROW", line, message: "row is missing or malformed `doc`", table }, kind: "error" };
|
|
78
|
+
}
|
|
79
|
+
const failure = validateRow(schema, table, doc);
|
|
80
|
+
if (failure !== void 0) {
|
|
81
|
+
return { error: { code: "VALIDATION_ERROR", line, message: failure, table }, kind: "error" };
|
|
82
|
+
}
|
|
83
|
+
const explicitId = typeof doc["_id"] === "string" ? doc["_id"] : void 0;
|
|
84
|
+
if (explicitId !== void 0 && await explicitIdConflicts(writer, args.exec, table, explicitId)) {
|
|
85
|
+
return { kind: "conflict" };
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
await writer.insert(table, doc, { allowExplicitId: true });
|
|
89
|
+
return { inserted: table, kind: "inserted" };
|
|
90
|
+
} catch (error) {
|
|
91
|
+
const code = error.code ?? "INSERT_FAILED";
|
|
92
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
93
|
+
return { error: { code, line, message, table }, kind: "error" };
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const importGlobalRows = async (writer, schema, args) => {
|
|
97
|
+
const errors = [];
|
|
98
|
+
const inserted = {};
|
|
99
|
+
let conflicts = 0;
|
|
100
|
+
let line = (args.startLine ?? 1) - 1;
|
|
101
|
+
for (const row of args.rows) {
|
|
102
|
+
line += 1;
|
|
103
|
+
const outcome = await importOneRow(writer, schema, args, row, line);
|
|
104
|
+
switch (outcome.kind) {
|
|
105
|
+
case "conflict": {
|
|
106
|
+
conflicts += 1;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case "error": {
|
|
110
|
+
errors.push(outcome.error);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
case "inserted": {
|
|
114
|
+
inserted[outcome.inserted] = (inserted[outcome.inserted] ?? 0) + 1;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { conflicts, errors, inserted };
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export { exportGlobalRows, importGlobalRows, selectGlobalTables };
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { runD1GlobalTableMigrations } from './createD1CtxDb-BMR8J0dT.mjs';
|
|
2
|
+
import { decodeGlobalRow } from '@lunora/sql-store';
|
|
3
|
+
|
|
4
|
+
const ensureGlobalTables = (exec, schema) => runD1GlobalTableMigrations(exec, schema);
|
|
5
|
+
const DEFAULT_PAGE_SIZE = 50;
|
|
6
|
+
const MAX_PAGE_SIZE = 500;
|
|
7
|
+
const DEFAULT_FACET_LIMIT = 30;
|
|
8
|
+
const MAX_FACET_LIMIT = 200;
|
|
9
|
+
const quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
10
|
+
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
11
|
+
const INTERNAL_TABLE = /^sqlite_|^_cf_|^d1_|^__cdc|__agg_|__rank_|__fts_/u;
|
|
12
|
+
const isInternalTable = (name) => INTERNAL_TABLE.test(name);
|
|
13
|
+
const SENSITIVE_COLUMN = /password|secret|token|hash|salt|credential/iu;
|
|
14
|
+
const listTableNames = async (exec) => {
|
|
15
|
+
const rows = await exec.all("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name", []);
|
|
16
|
+
return rows.map((row) => String(row["name"])).filter((name) => !isInternalTable(name));
|
|
17
|
+
};
|
|
18
|
+
const countRows = async (exec, quotedTable, whereSql = "", whereParams = []) => {
|
|
19
|
+
const rows = await exec.all(`SELECT COUNT(*) AS c FROM ${quotedTable}${whereSql}`, whereParams);
|
|
20
|
+
return Number(rows[0]?.["c"] ?? 0);
|
|
21
|
+
};
|
|
22
|
+
const physicalColumnName = (schema, table, displayColumn) => schema.tables[table] !== void 0 && displayColumn === "_id" ? "id" : displayColumn;
|
|
23
|
+
const buildEqPredicate = (schema, table, displayColumns, filters) => {
|
|
24
|
+
if (filters === void 0 || filters.length === 0) {
|
|
25
|
+
return void 0;
|
|
26
|
+
}
|
|
27
|
+
const clauses = [];
|
|
28
|
+
const params = [];
|
|
29
|
+
for (const filter of filters) {
|
|
30
|
+
if (!displayColumns.includes(filter.column)) {
|
|
31
|
+
throw Object.assign(new Error(`unknown column: ${filter.column}`), { code: "UNKNOWN_COLUMN", name: "LunoraError", status: 404 });
|
|
32
|
+
}
|
|
33
|
+
const quoted = quoteIdentifier(physicalColumnName(schema, table, filter.column));
|
|
34
|
+
if (filter.value === null || filter.value === void 0) {
|
|
35
|
+
clauses.push(`${quoted} IS NULL`);
|
|
36
|
+
} else {
|
|
37
|
+
clauses.push(`${quoted} = ?`);
|
|
38
|
+
params.push(filter.value);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { params, where: clauses.join(" AND ") };
|
|
42
|
+
};
|
|
43
|
+
const decodeRow = (schema, table, row) => {
|
|
44
|
+
const definition = schema.tables[table];
|
|
45
|
+
if (definition) {
|
|
46
|
+
return decodeGlobalRow(definition, row);
|
|
47
|
+
}
|
|
48
|
+
const redacted = {};
|
|
49
|
+
for (const [key, value] of Object.entries(row)) {
|
|
50
|
+
redacted[key] = value !== null && value !== void 0 && SENSITIVE_COLUMN.test(key) ? "•••" : value;
|
|
51
|
+
}
|
|
52
|
+
return redacted;
|
|
53
|
+
};
|
|
54
|
+
const resolveColumns = async (exec, schema, table) => {
|
|
55
|
+
const definition = schema.tables[table];
|
|
56
|
+
if (definition) {
|
|
57
|
+
return ["_id", "_creationTime", ...Object.keys(definition.shape)];
|
|
58
|
+
}
|
|
59
|
+
const info = await exec.all(`PRAGMA table_info(${quoteIdentifier(table)})`, []);
|
|
60
|
+
return info.map((column) => String(column["name"]));
|
|
61
|
+
};
|
|
62
|
+
const resolveReferences = async (exec, schema, table) => {
|
|
63
|
+
if (schema.tables[table]) {
|
|
64
|
+
return void 0;
|
|
65
|
+
}
|
|
66
|
+
const rows = await exec.all(`PRAGMA foreign_key_list(${quoteIdentifier(table)})`, []);
|
|
67
|
+
if (rows.length === 0) {
|
|
68
|
+
return void 0;
|
|
69
|
+
}
|
|
70
|
+
const references = {};
|
|
71
|
+
for (const row of rows) {
|
|
72
|
+
const from = String(row["from"]);
|
|
73
|
+
const target = String(row["table"]);
|
|
74
|
+
references[from] ??= target;
|
|
75
|
+
}
|
|
76
|
+
return references;
|
|
77
|
+
};
|
|
78
|
+
const listGlobalTables = async (exec, schema) => {
|
|
79
|
+
await ensureGlobalTables(exec, schema);
|
|
80
|
+
const names = await listTableNames(exec);
|
|
81
|
+
return Promise.all(
|
|
82
|
+
names.map(async (name) => {
|
|
83
|
+
return { name, rowCount: await countRows(exec, quoteIdentifier(name)) };
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
const readGlobalTablePage = async (exec, schema, options) => {
|
|
88
|
+
const { table } = options;
|
|
89
|
+
await ensureGlobalTables(exec, schema);
|
|
90
|
+
const tableNames = await listTableNames(exec);
|
|
91
|
+
if (!tableNames.includes(table)) {
|
|
92
|
+
throw Object.assign(new Error(`unknown table: ${table}`), { code: "UNKNOWN_TABLE", name: "LunoraError", status: 404 });
|
|
93
|
+
}
|
|
94
|
+
const limit = clamp(Math.trunc(options.limit ?? DEFAULT_PAGE_SIZE), 1, MAX_PAGE_SIZE);
|
|
95
|
+
const offset = Math.max(0, Math.trunc(options.offset ?? 0));
|
|
96
|
+
const quoted = quoteIdentifier(table);
|
|
97
|
+
const columns = await resolveColumns(exec, schema, table);
|
|
98
|
+
const predicate = buildEqPredicate(schema, table, columns, options.filters);
|
|
99
|
+
const whereSql = predicate === void 0 ? "" : ` WHERE ${predicate.where}`;
|
|
100
|
+
const whereParams = predicate?.params ?? [];
|
|
101
|
+
const total = await countRows(exec, quoted, whereSql, whereParams);
|
|
102
|
+
const raw = await exec.all(`SELECT * FROM ${quoted}${whereSql} LIMIT ? OFFSET ?`, [...whereParams, limit, offset]);
|
|
103
|
+
const rows = raw.map((row) => decodeRow(schema, table, row));
|
|
104
|
+
const references = await resolveReferences(exec, schema, table);
|
|
105
|
+
return references === void 0 ? { columns, rows, total } : { columns, refs: references, rows, total };
|
|
106
|
+
};
|
|
107
|
+
const facetGlobalColumn = async (exec, schema, options) => {
|
|
108
|
+
const { column, table } = options;
|
|
109
|
+
await ensureGlobalTables(exec, schema);
|
|
110
|
+
const tableNames = await listTableNames(exec);
|
|
111
|
+
if (!tableNames.includes(table)) {
|
|
112
|
+
throw Object.assign(new Error(`unknown table: ${table}`), { code: "UNKNOWN_TABLE", name: "LunoraError", status: 404 });
|
|
113
|
+
}
|
|
114
|
+
const columns = await resolveColumns(exec, schema, table);
|
|
115
|
+
if (!columns.includes(column)) {
|
|
116
|
+
throw Object.assign(new Error(`unknown column: ${column}`), { code: "UNKNOWN_COLUMN", name: "LunoraError", status: 404 });
|
|
117
|
+
}
|
|
118
|
+
const quoted = quoteIdentifier(table);
|
|
119
|
+
const predicate = buildEqPredicate(schema, table, columns, options.filters);
|
|
120
|
+
const whereSql = predicate === void 0 ? "" : ` WHERE ${predicate.where}`;
|
|
121
|
+
const whereParams = predicate?.params ?? [];
|
|
122
|
+
if (schema.tables[table] === void 0 && SENSITIVE_COLUMN.test(column)) {
|
|
123
|
+
const total = await countRows(exec, quoted, whereSql, whereParams);
|
|
124
|
+
return { truncated: false, values: total === 0 ? [] : [{ count: total, value: "•••" }] };
|
|
125
|
+
}
|
|
126
|
+
const limit = clamp(Math.trunc(options.limit ?? DEFAULT_FACET_LIMIT), 1, MAX_FACET_LIMIT);
|
|
127
|
+
const physical = quoteIdentifier(physicalColumnName(schema, table, column));
|
|
128
|
+
const rows = await exec.all(`SELECT ${physical} AS value, COUNT(*) AS count FROM ${quoted}${whereSql} GROUP BY ${physical} ORDER BY count DESC LIMIT ?`, [
|
|
129
|
+
...whereParams,
|
|
130
|
+
limit + 1
|
|
131
|
+
]);
|
|
132
|
+
const truncated = rows.length > limit;
|
|
133
|
+
const kept = truncated ? rows.slice(0, limit) : rows;
|
|
134
|
+
return {
|
|
135
|
+
truncated,
|
|
136
|
+
values: kept.map((row) => {
|
|
137
|
+
return { count: Number(row["count"]), value: row["value"] };
|
|
138
|
+
})
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export { facetGlobalColumn, listGlobalTables, readGlobalTablePage };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { sqliteEncode, sqliteDecode } from '@lunora/sql-store';
|
|
2
|
+
import { sql } from 'drizzle-orm';
|
|
3
|
+
import { sqlAffinityForKind } from '../dialect.mjs';
|
|
4
|
+
|
|
5
|
+
const UNIQUE_VIOLATION_RE = /unique constraint failed/iu;
|
|
6
|
+
const sqliteDialect = {
|
|
7
|
+
companionTypes: {
|
|
8
|
+
autoincrementPrimaryKey: "INTEGER PRIMARY KEY AUTOINCREMENT",
|
|
9
|
+
integer: "INTEGER",
|
|
10
|
+
key: "TEXT",
|
|
11
|
+
real: "REAL",
|
|
12
|
+
text: "TEXT"
|
|
13
|
+
},
|
|
14
|
+
columnType: (kind) => sqlAffinityForKind(kind),
|
|
15
|
+
decode: (value, kind) => sqliteDecode(value, kind),
|
|
16
|
+
encode: (value) => sqliteEncode(value),
|
|
17
|
+
frameworkColumns: () => [
|
|
18
|
+
{ name: "id", type: "TEXT PRIMARY KEY" },
|
|
19
|
+
{ name: "_creationTime", type: "REAL NOT NULL" }
|
|
20
|
+
],
|
|
21
|
+
isUniqueViolation: (error) => error instanceof Error && UNIQUE_VIOLATION_RE.test(error.message),
|
|
22
|
+
name: "sqlite",
|
|
23
|
+
supportsReturning: true,
|
|
24
|
+
tableExists: (table) => sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${table}`
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export { sqliteDialect as default };
|
package/package.json
CHANGED
|
@@ -1,31 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/d1",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "1.0.0-alpha.2",
|
|
4
4
|
"description": "D1 adapter for Lunora .global() tables, wrapping the Sessions API for read-your-writes",
|
|
5
|
-
"
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cloudflare",
|
|
7
|
+
"d1",
|
|
8
|
+
"durable-objects",
|
|
9
|
+
"lunora",
|
|
10
|
+
"migrations",
|
|
11
|
+
"sessions-api",
|
|
12
|
+
"sqlite",
|
|
13
|
+
"workers"
|
|
14
|
+
],
|
|
6
15
|
"homepage": "https://lunora.sh",
|
|
16
|
+
"bugs": "https://github.com/anolilab/lunora/issues",
|
|
17
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
18
|
+
"author": {
|
|
19
|
+
"name": "Daniel Bannert",
|
|
20
|
+
"email": "d.bannert@anolilab.de"
|
|
21
|
+
},
|
|
7
22
|
"repository": {
|
|
8
23
|
"type": "git",
|
|
9
24
|
"url": "git+https://github.com/anolilab/lunora.git",
|
|
10
25
|
"directory": "packages/d1"
|
|
11
26
|
},
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
"cloudflare",
|
|
18
|
-
"workers",
|
|
19
|
-
"durable-objects",
|
|
20
|
-
"d1",
|
|
21
|
-
"sessions-api",
|
|
22
|
-
"sqlite",
|
|
23
|
-
"migrations"
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE.md",
|
|
31
|
+
"__assets__"
|
|
24
32
|
],
|
|
33
|
+
"type": "module",
|
|
34
|
+
"sideEffects": false,
|
|
35
|
+
"main": "./dist/index.mjs",
|
|
36
|
+
"module": "./dist/index.mjs",
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"exports": {
|
|
39
|
+
".": {
|
|
40
|
+
"types": "./dist/index.d.ts",
|
|
41
|
+
"import": "./dist/index.mjs"
|
|
42
|
+
},
|
|
43
|
+
"./dialect": {
|
|
44
|
+
"types": "./dist/dialect.d.ts",
|
|
45
|
+
"import": "./dist/dialect.mjs"
|
|
46
|
+
},
|
|
47
|
+
"./package.json": "./package.json"
|
|
48
|
+
},
|
|
25
49
|
"publishConfig": {
|
|
26
50
|
"access": "public"
|
|
27
51
|
},
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@lunora/do": "1.0.0-alpha.2",
|
|
54
|
+
"@lunora/sql-store": "1.0.0-alpha.2",
|
|
55
|
+
"drizzle-orm": "^0.45.2"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": "^22.15.0 || >=24.11.0"
|
|
59
|
+
}
|
|
31
60
|
}
|