@saacms/storage-libsql 0.1.0
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/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +175 -0
- package/dist/libsql-adapter.d.ts +65 -0
- package/dist/libsql-adapter.d.ts.map +1 -0
- package/package.json +36 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @saacms/storage-libsql — public surface.
|
|
3
|
+
*
|
|
4
|
+
* libSQL (Turso / embedded SQLite) implementation of the saacms
|
|
5
|
+
* `RowStorageAdapter` interface. Mirrors `@saacms/storage-d1`'s structure
|
|
6
|
+
* exactly; only the driver call-shape differs (libSQL `execute({sql,args})`
|
|
7
|
+
* vs D1 `prepare().bind().all()/run()`).
|
|
8
|
+
*
|
|
9
|
+
* The host app supplies a real `createClient({url, authToken})` instance from
|
|
10
|
+
* `@libsql/client`; it satisfies `LibsqlClientLike` structurally — no runtime
|
|
11
|
+
* dep on `@libsql/client` is required here.
|
|
12
|
+
*/
|
|
13
|
+
export { libsqlAdapter, snakeToCamel, RowStorageError } from "./libsql-adapter.ts";
|
|
14
|
+
export type { LibsqlAdapterOpts, LibsqlClientLike, FilterOpValue, RowStorageAdapter, ListOpts, ListResult, } from "./libsql-adapter.ts";
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAClF,YAAY,EACV,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,iBAAiB,EACjB,QAAQ,EACR,UAAU,GACX,MAAM,qBAAqB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// src/libsql-adapter.ts
|
|
2
|
+
import { camelToSnakeCase, UniqueConstraintError, RowStorageError } from "@saacms/core";
|
|
3
|
+
function snakeToCamel(name) {
|
|
4
|
+
return name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
5
|
+
}
|
|
6
|
+
function keysToSnake(row) {
|
|
7
|
+
const out = {};
|
|
8
|
+
for (const [k, v] of Object.entries(row))
|
|
9
|
+
out[camelToSnakeCase(k)] = v;
|
|
10
|
+
return out;
|
|
11
|
+
}
|
|
12
|
+
function rowToCamel(row) {
|
|
13
|
+
const out = {};
|
|
14
|
+
for (const [k, v] of Object.entries(row)) {
|
|
15
|
+
if (v !== null)
|
|
16
|
+
out[snakeToCamel(k)] = v;
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
var IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
|
|
21
|
+
function assertSafeIdent(name) {
|
|
22
|
+
if (typeof name !== "string" || !IDENT_RE.test(name)) {
|
|
23
|
+
throw new RowStorageError("MISCONFIGURED", `unsafe identifier: ${JSON.stringify(name)}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function quoteIdent(name) {
|
|
27
|
+
return `"${name}"`;
|
|
28
|
+
}
|
|
29
|
+
var FILTER_OP_SQL = {
|
|
30
|
+
eq: "=",
|
|
31
|
+
ne: "<>",
|
|
32
|
+
gt: ">",
|
|
33
|
+
gte: ">=",
|
|
34
|
+
lt: "<",
|
|
35
|
+
lte: "<="
|
|
36
|
+
};
|
|
37
|
+
function isFilterOpValue(v) {
|
|
38
|
+
if (typeof v !== "object" || v === null)
|
|
39
|
+
return false;
|
|
40
|
+
const candidate = v;
|
|
41
|
+
const op = candidate["op"];
|
|
42
|
+
return typeof op === "string" && op in FILTER_OP_SQL && "value" in candidate;
|
|
43
|
+
}
|
|
44
|
+
function isUniqueViolation(cause) {
|
|
45
|
+
if (cause == null)
|
|
46
|
+
return false;
|
|
47
|
+
const msg = (cause instanceof Error ? cause.message : String(cause)).toLowerCase();
|
|
48
|
+
return msg.includes("unique constraint failed");
|
|
49
|
+
}
|
|
50
|
+
var DEFAULT_LIMIT = 50;
|
|
51
|
+
var MAX_LIMIT = 1000;
|
|
52
|
+
function libsqlAdapter(opts) {
|
|
53
|
+
const { client } = opts;
|
|
54
|
+
return {
|
|
55
|
+
async getById(table, id) {
|
|
56
|
+
assertSafeIdent(table);
|
|
57
|
+
const sql = `SELECT * FROM ${quoteIdent(table)} WHERE id = ? LIMIT 1`;
|
|
58
|
+
try {
|
|
59
|
+
const result = await client.execute({ sql, args: [id] });
|
|
60
|
+
const row = result.rows[0] ?? null;
|
|
61
|
+
return row ? rowToCamel(row) : null;
|
|
62
|
+
} catch (cause) {
|
|
63
|
+
throw new RowStorageError("QUERY_FAILED", `libsqlAdapter.getById failed for table "${table}"`, cause);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
async list(table, listOpts) {
|
|
67
|
+
assertSafeIdent(table);
|
|
68
|
+
const whereClauses = [];
|
|
69
|
+
const args = [];
|
|
70
|
+
if (listOpts?.where) {
|
|
71
|
+
for (const [col, val] of Object.entries(listOpts.where)) {
|
|
72
|
+
const snakeCol = camelToSnakeCase(col);
|
|
73
|
+
assertSafeIdent(snakeCol);
|
|
74
|
+
if (isFilterOpValue(val)) {
|
|
75
|
+
whereClauses.push(`${quoteIdent(snakeCol)} ${FILTER_OP_SQL[val.op]} ?`);
|
|
76
|
+
args.push(val.value);
|
|
77
|
+
} else {
|
|
78
|
+
whereClauses.push(`${quoteIdent(snakeCol)} = ?`);
|
|
79
|
+
args.push(val);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (listOpts?.cursor !== undefined) {
|
|
84
|
+
whereClauses.push(`id > ?`);
|
|
85
|
+
args.push(listOpts.cursor);
|
|
86
|
+
}
|
|
87
|
+
let orderBy = `ORDER BY id ASC`;
|
|
88
|
+
if (listOpts?.orderBy && listOpts.orderBy.length > 0) {
|
|
89
|
+
const parts = [];
|
|
90
|
+
for (const { col, dir } of listOpts.orderBy) {
|
|
91
|
+
const snakeCol = camelToSnakeCase(col);
|
|
92
|
+
assertSafeIdent(snakeCol);
|
|
93
|
+
parts.push(`${quoteIdent(snakeCol)} ${dir === "desc" ? "DESC" : "ASC"}`);
|
|
94
|
+
}
|
|
95
|
+
orderBy = `ORDER BY ${parts.join(", ")}`;
|
|
96
|
+
}
|
|
97
|
+
const rawLimit = listOpts?.limit ?? DEFAULT_LIMIT;
|
|
98
|
+
const limit = Math.max(1, Math.min(MAX_LIMIT, rawLimit | 0));
|
|
99
|
+
const whereSql = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
|
|
100
|
+
const sql = `SELECT * FROM ${quoteIdent(table)} ${whereSql} ${orderBy} LIMIT ?`.replace(/\s+/g, " ").trim();
|
|
101
|
+
args.push(limit);
|
|
102
|
+
try {
|
|
103
|
+
const result = await client.execute({ sql, args });
|
|
104
|
+
const rows = result.rows.map((r) => rowToCamel(r));
|
|
105
|
+
const nextCursorVal = rows.length === limit ? rows[rows.length - 1]?.id ?? null : null;
|
|
106
|
+
return {
|
|
107
|
+
rows,
|
|
108
|
+
nextCursor: nextCursorVal === null ? null : String(nextCursorVal)
|
|
109
|
+
};
|
|
110
|
+
} catch (cause) {
|
|
111
|
+
throw new RowStorageError("QUERY_FAILED", `libsqlAdapter.list failed for table "${table}"`, cause);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
async insert(table, row) {
|
|
115
|
+
assertSafeIdent(table);
|
|
116
|
+
const snakeRow = keysToSnake(row);
|
|
117
|
+
const entries = Object.entries(snakeRow);
|
|
118
|
+
const hasId = "id" in snakeRow;
|
|
119
|
+
const id = hasId ? String(snakeRow.id) : crypto.randomUUID();
|
|
120
|
+
const finalEntries = hasId ? entries : [["id", id], ...entries];
|
|
121
|
+
const cols = [];
|
|
122
|
+
const args = [];
|
|
123
|
+
for (const [k, v] of finalEntries) {
|
|
124
|
+
assertSafeIdent(k);
|
|
125
|
+
cols.push(quoteIdent(k));
|
|
126
|
+
args.push(v);
|
|
127
|
+
}
|
|
128
|
+
const placeholders = cols.map(() => "?").join(", ");
|
|
129
|
+
const sql = `INSERT INTO ${quoteIdent(table)} (${cols.join(", ")}) VALUES (${placeholders})`;
|
|
130
|
+
try {
|
|
131
|
+
await client.execute({ sql, args });
|
|
132
|
+
return { id };
|
|
133
|
+
} catch (cause) {
|
|
134
|
+
if (isUniqueViolation(cause))
|
|
135
|
+
throw new UniqueConstraintError(table, cause);
|
|
136
|
+
throw new RowStorageError("INSERT_FAILED", `libsqlAdapter.insert failed for table "${table}"`, cause);
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
async update(table, id, patch) {
|
|
140
|
+
assertSafeIdent(table);
|
|
141
|
+
const snakePatch = keysToSnake(patch);
|
|
142
|
+
snakePatch["updated_at"] = new Date().toISOString();
|
|
143
|
+
const sets = [];
|
|
144
|
+
const args = [];
|
|
145
|
+
for (const [k, v] of Object.entries(snakePatch)) {
|
|
146
|
+
assertSafeIdent(k);
|
|
147
|
+
sets.push(`${quoteIdent(k)} = ?`);
|
|
148
|
+
args.push(v);
|
|
149
|
+
}
|
|
150
|
+
args.push(id);
|
|
151
|
+
const sql = `UPDATE ${quoteIdent(table)} SET ${sets.join(", ")} WHERE id = ?`;
|
|
152
|
+
try {
|
|
153
|
+
await client.execute({ sql, args });
|
|
154
|
+
} catch (cause) {
|
|
155
|
+
if (isUniqueViolation(cause))
|
|
156
|
+
throw new UniqueConstraintError(table, cause);
|
|
157
|
+
throw new RowStorageError("UPDATE_FAILED", `libsqlAdapter.update failed for table "${table}"`, cause);
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
async delete(table, id) {
|
|
161
|
+
assertSafeIdent(table);
|
|
162
|
+
const sql = `DELETE FROM ${quoteIdent(table)} WHERE id = ?`;
|
|
163
|
+
try {
|
|
164
|
+
await client.execute({ sql, args: [id] });
|
|
165
|
+
} catch (cause) {
|
|
166
|
+
throw new RowStorageError("DELETE_FAILED", `libsqlAdapter.delete failed for table "${table}"`, cause);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
export {
|
|
172
|
+
snakeToCamel,
|
|
173
|
+
libsqlAdapter,
|
|
174
|
+
RowStorageError
|
|
175
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* libSQL row-storage adapter — implements the saacms `RowStorageAdapter`
|
|
3
|
+
* interface over a libSQL client (Turso / embedded SQLite).
|
|
4
|
+
*
|
|
5
|
+
* `LibsqlClientLike` is a structural interface — the minimal subset of
|
|
6
|
+
* `@libsql/client`'s `Client` this adapter uses. No runtime dependency on
|
|
7
|
+
* `@libsql/client` is required; the host app supplies a real
|
|
8
|
+
* `createClient({url,authToken})` instance and it slots in structurally.
|
|
9
|
+
*
|
|
10
|
+
* Real @libsql/client shape (v0.14+/v1.x) — confirmed from published TypeScript
|
|
11
|
+
* declarations at github.com/tursodatabase/libsql-client-ts:
|
|
12
|
+
*
|
|
13
|
+
* interface Client {
|
|
14
|
+
* execute(stmt: string | { sql: string; args?: InValue[] | Record<string, InValue> }): Promise<ResultSet>
|
|
15
|
+
* }
|
|
16
|
+
* interface ResultSet { columns: string[]; rows: Row[]; rowsAffected: number; ... }
|
|
17
|
+
* interface Row { [key: string]: Value; [key: number]: Value }
|
|
18
|
+
* type Value = null | string | number | bigint | ArrayBuffer | Uint8Array
|
|
19
|
+
*
|
|
20
|
+
* Structural compatibility: `Row` has `[key: string]: Value` and Value ⊆
|
|
21
|
+
* unknown, so `Row[]` is assignable to `Array<Record<string, unknown>>` ✓.
|
|
22
|
+
* TypeScript's bivariant method checking (method shorthand, not property-arrow)
|
|
23
|
+
* means a real Client (execute param: string | InStatementObject) is assignable
|
|
24
|
+
* to LibsqlClientLike (execute param: { sql, args? }) without a compile dep ✓.
|
|
25
|
+
*
|
|
26
|
+
* Anti-corruption boundary (ADR 0020): callers speak camelCase domain keys;
|
|
27
|
+
* the physical SQLite/Turso schema is snake_case (ADR 0005). This adapter is
|
|
28
|
+
* the only place the two conventions meet — camelCase→snake_case on every
|
|
29
|
+
* write/query path, snake_case→camelCase on every read path.
|
|
30
|
+
* `camelToSnakeCase` is imported from @saacms/core (single source of truth —
|
|
31
|
+
* never re-implement the regex). Server-injected timestamp columns
|
|
32
|
+
* (`created_at`/`updated_at`) surface to callers as `createdAt`/`updatedAt`,
|
|
33
|
+
* restoring the weak-ETag field the runtime reads (ADR 0021 §6 / RFC 9110).
|
|
34
|
+
*/
|
|
35
|
+
import { RowStorageError } from "@saacms/core";
|
|
36
|
+
import type { RowStorageAdapter, ListOpts, ListResult } from "@saacms/core";
|
|
37
|
+
export { RowStorageError };
|
|
38
|
+
export type { RowStorageAdapter, ListOpts, ListResult };
|
|
39
|
+
/** Inverse of `camelToSnakeCase`: `updated_at` → `updatedAt`, `id` → `id`. */
|
|
40
|
+
export declare function snakeToCamel(name: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Minimal structural subset of `@libsql/client`'s `Client` that this adapter
|
|
43
|
+
* uses. No runtime dependency on `@libsql/client` — the host provides a real
|
|
44
|
+
* instance and it slots in structurally (see module-level comment).
|
|
45
|
+
*/
|
|
46
|
+
export interface LibsqlClientLike {
|
|
47
|
+
execute(stmt: {
|
|
48
|
+
sql: string;
|
|
49
|
+
args?: unknown[];
|
|
50
|
+
}): Promise<{
|
|
51
|
+
rows: Array<Record<string, unknown>>;
|
|
52
|
+
rowsAffected: number;
|
|
53
|
+
}>;
|
|
54
|
+
}
|
|
55
|
+
export interface LibsqlAdapterOpts {
|
|
56
|
+
readonly client: LibsqlClientLike;
|
|
57
|
+
}
|
|
58
|
+
type FilterOp = "eq" | "ne" | "gt" | "gte" | "lt" | "lte";
|
|
59
|
+
/** Discriminated operator filter produced by list-route and consumed here. */
|
|
60
|
+
export interface FilterOpValue {
|
|
61
|
+
readonly op: FilterOp;
|
|
62
|
+
readonly value: unknown;
|
|
63
|
+
}
|
|
64
|
+
export declare function libsqlAdapter(opts: LibsqlAdapterOpts): RowStorageAdapter;
|
|
65
|
+
//# sourceMappingURL=libsql-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"libsql-adapter.d.ts","sourceRoot":"","sources":["../src/libsql-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAA2C,eAAe,EAAE,MAAM,cAAc,CAAA;AACvF,OAAO,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAI3E,OAAO,EAAE,eAAe,EAAE,CAAA;AAC1B,YAAY,EAAE,iBAAiB,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAA;AAavD,8EAA8E;AAC9E,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjD;AA6BD;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,IAAI,EAAE;QACZ,GAAG,EAAE,MAAM,CAAA;QACX,IAAI,CAAC,EAAE,OAAO,EAAE,CAAA;KACjB,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC5E;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAA;CAClC;AAsCD,KAAK,QAAQ,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,CAAA;AAEzD,8EAA8E;AAC9E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CACxB;AAwCD,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,iBAAiB,CAuKxE"}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@saacms/storage-libsql",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"import": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"default": "./dist/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc --build",
|
|
18
|
+
"typecheck": "tsc --build --noEmit",
|
|
19
|
+
"prepack": "cp package.json package.json.pack-bak && bun run ../../scripts/prepack-pkg.ts",
|
|
20
|
+
"postpack": "mv package.json.pack-bak package.json"
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@saacms/core": "workspace:*"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@saacms/storage-d1": "workspace:*",
|
|
30
|
+
"@types/bun": "latest",
|
|
31
|
+
"effect": "^3.10.0",
|
|
32
|
+
"typescript": "^5.7.0"
|
|
33
|
+
},
|
|
34
|
+
"main": "./dist/index.js",
|
|
35
|
+
"types": "./dist/index.d.ts"
|
|
36
|
+
}
|