totalum-sdk 0.1.0-dev.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/README.md ADDED
@@ -0,0 +1,14 @@
1
+ # totalum-sdk
2
+
3
+ The SDK Totalum v2 apps import. Wave 1 ships `totalum-sdk/d1`: a `D1Database` for the project's own database wherever the code runs — the bound `TOTALUM_DB` inside a Cloudflare Worker, HTTPS to SDK-API from a sandbox or a laptop (`TOTALUM_PROJECT_KEY`, optional `TOTALUM_SDK_API_URL`, `TOTALUM_ENV=dev|live`), and the Turso adapter when `TOTALUM_DB_PROVIDER=turso`.
4
+
5
+ ```ts
6
+ import { drizzle } from 'drizzle-orm/d1';
7
+ import { totalumD1 } from 'totalum-sdk/d1';
8
+
9
+ export const db = drizzle(totalumD1()); // no I/O until the first statement
10
+ ```
11
+
12
+ `totalumD1({ mode: 'migrate' })` is for `scripts/migrate.ts` only: it enables DDL (`x-totalum-migrate: 1`) and `exec()`. Failures are `TotalumD1Error` (`errorCode`, `status`, `details`, `requestId`); SQLite failures keep the binding's `D1_ERROR: …` message byte for byte.
13
+
14
+ Pre-release (`0.1.0-dev.N`, dist-tag `dev`): the API may change until 0.1.0.
@@ -0,0 +1,27 @@
1
+ import type { ErrorBody, ErrorCode } from '@speed-paradigm/totalum-contracts';
2
+ /** Client-side codes of plan 05 §7.2 that never travel on the wire, so they are not in the contracts enum. */
3
+ export type TotalumD1ClientCode = 'SDK_NOT_CONFIGURED' | 'NOT_SUPPORTED' | 'BATCH_FOREIGN_STATEMENT' | 'NETWORK_ERROR' | 'RESPONSE_PARSE_ERROR';
4
+ export type TotalumD1ErrorCode = ErrorCode | TotalumD1ClientCode;
5
+ /**
6
+ * The database twin of `TotalumError` (plan 05 §3.5): `message` is `D1_ERROR: …`-shaped so it reads like a
7
+ * native failure — byte-identical to the binding's message when the failure is SQLite's.
8
+ */
9
+ export declare class TotalumD1Error extends Error {
10
+ readonly name = "TotalumD1Error";
11
+ readonly errorCode: TotalumD1ErrorCode;
12
+ /** HTTP status; 0 for network errors, 500 for client-side refusals. */
13
+ readonly status: number;
14
+ readonly details: Record<string, unknown> | undefined;
15
+ readonly requestId: string | undefined;
16
+ constructor(errorCode: TotalumD1ErrorCode, message: string, options?: {
17
+ status?: number;
18
+ details?: Record<string, unknown>;
19
+ requestId?: string;
20
+ });
21
+ /** A client-side refusal (`D1_ERROR: <code>: <message>`). */
22
+ static client(errorCode: TotalumD1ClientCode | 'EXEC_REQUIRES_MIGRATE_MODE', message: string): TotalumD1Error;
23
+ /** The SDK-API error envelope → typed error; `SQL_ERROR` rebuilds the native message from `sqliteMessage`. */
24
+ static fromWire(errors: ErrorBody, status: number, requestId: string | undefined): TotalumD1Error;
25
+ }
26
+ export declare function isTotalumD1Error(e: unknown): e is TotalumD1Error;
27
+ export declare function isRecord(v: unknown): v is Record<string, unknown>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The database twin of `TotalumError` (plan 05 §3.5): `message` is `D1_ERROR: …`-shaped so it reads like a
3
+ * native failure — byte-identical to the binding's message when the failure is SQLite's.
4
+ */
5
+ export class TotalumD1Error extends Error {
6
+ name = 'TotalumD1Error';
7
+ errorCode;
8
+ /** HTTP status; 0 for network errors, 500 for client-side refusals. */
9
+ status;
10
+ details;
11
+ requestId;
12
+ constructor(errorCode, message, options = {}) {
13
+ super(message);
14
+ this.errorCode = errorCode;
15
+ this.status = options.status ?? 500;
16
+ this.details = options.details;
17
+ this.requestId = options.requestId;
18
+ }
19
+ /** A client-side refusal (`D1_ERROR: <code>: <message>`). */
20
+ static client(errorCode, message) {
21
+ return new TotalumD1Error(errorCode, `D1_ERROR: ${errorCode}: ${message}`);
22
+ }
23
+ /** The SDK-API error envelope → typed error; `SQL_ERROR` rebuilds the native message from `sqliteMessage`. */
24
+ static fromWire(errors, status, requestId) {
25
+ const details = isRecord(errors.errorDetails) ? errors.errorDetails : undefined;
26
+ const sqlite = details?.['sqliteMessage'];
27
+ const message = errors.errorCode === 'SQL_ERROR' && typeof sqlite === 'string'
28
+ ? `D1_ERROR: ${sqlite}`
29
+ : `D1_ERROR: ${errors.errorCode}: ${errors.errorMessage}`;
30
+ const init = { status, ...(details && { details }), ...(requestId !== undefined && { requestId }) };
31
+ return new TotalumD1Error(errors.errorCode, message, init);
32
+ }
33
+ }
34
+ export function isTotalumD1Error(e) {
35
+ return e instanceof TotalumD1Error;
36
+ }
37
+ export function isRecord(v) {
38
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
39
+ }
@@ -0,0 +1,10 @@
1
+ import type { Executor } from './types.js';
2
+ export interface HttpsConfig {
3
+ apiUrl: string;
4
+ key: string;
5
+ env: 'dev' | 'live';
6
+ migrate: boolean;
7
+ destructive: boolean;
8
+ }
9
+ /** The `D1Database` half that speaks `POST /v1/db/query|batch` (plan 04 §3.2; contracts `db.ts`). */
10
+ export declare function httpsExecutor(config: HttpsConfig): Executor;
@@ -0,0 +1,103 @@
1
+ import { TotalumD1Error } from './errors.js';
2
+ import { assertBindable, pickFirst } from './sql.js';
3
+ /**
4
+ * The wire headers the driver sends, as literals so the published package has no runtime dependency (plan 05
5
+ * §1); `satisfies` over contracts' `as const` object fails the build if either side ever drifts.
6
+ */
7
+ const HEADERS = {
8
+ env: 'x-totalum-env',
9
+ migrate: 'x-totalum-migrate',
10
+ migrateDestructive: 'x-totalum-migrate-destructive',
11
+ };
12
+ /** The `D1Database` half that speaks `POST /v1/db/query|batch` (plan 04 §3.2; contracts `db.ts`). */
13
+ export function httpsExecutor(config) {
14
+ const headers = {
15
+ authorization: `Bearer ${config.key}`,
16
+ 'content-type': 'application/json',
17
+ [HEADERS.env]: config.env,
18
+ };
19
+ if (config.migrate)
20
+ headers[HEADERS.migrate] = '1';
21
+ if (config.migrate && config.destructive)
22
+ headers[HEADERS.migrateDestructive] = '1';
23
+ async function call(path, body) {
24
+ let response;
25
+ try {
26
+ response = await fetch(config.apiUrl + path, { method: 'POST', headers, body: JSON.stringify(body) });
27
+ }
28
+ catch (e) {
29
+ const reason = e instanceof Error ? e.message : String(e);
30
+ throw new TotalumD1Error('NETWORK_ERROR', `D1_ERROR: NETWORK_ERROR: ${reason}`, { status: 0 });
31
+ }
32
+ let payload;
33
+ try {
34
+ payload = await response.json();
35
+ }
36
+ catch {
37
+ throw new TotalumD1Error('RESPONSE_PARSE_ERROR', `D1_ERROR: RESPONSE_PARSE_ERROR: HTTP ${String(response.status)} without a JSON envelope`, {
38
+ status: response.status,
39
+ });
40
+ }
41
+ const envelope = payload;
42
+ if (envelope.errors !== null) {
43
+ throw TotalumD1Error.fromWire(envelope.errors, response.status, envelope.metadata?.requestId);
44
+ }
45
+ return envelope.data;
46
+ }
47
+ class HttpsStatement {
48
+ sql;
49
+ params;
50
+ constructor(sql, params = []) {
51
+ this.sql = sql;
52
+ this.params = params;
53
+ }
54
+ bind(...values) {
55
+ assertBindable(values);
56
+ return new HttpsStatement(this.sql, values.map(encodeParam));
57
+ }
58
+ query(mode, columnNames) {
59
+ const statement = { sql: this.sql, params: this.params, mode };
60
+ if (columnNames)
61
+ statement.columnNames = true;
62
+ return call('/v1/db/query', statement);
63
+ }
64
+ async first(colName) {
65
+ return pickFirst((await this.query('first')).results, colName);
66
+ }
67
+ run() {
68
+ return this.query('run');
69
+ }
70
+ all() {
71
+ return this.query('all');
72
+ }
73
+ async raw(options) {
74
+ return (await this.query('raw', options?.columnNames === true ? true : undefined)).results;
75
+ }
76
+ }
77
+ return {
78
+ prepare: (sql) => new HttpsStatement(sql),
79
+ async batch(statements) {
80
+ const body = {
81
+ statements: statements.map((s) => ({
82
+ sql: s.sql,
83
+ params: s.params,
84
+ })),
85
+ };
86
+ return (await call('/v1/db/batch', body)).results;
87
+ },
88
+ };
89
+ }
90
+ /** Bytes travel as `{ $bytes: base64 }` (plan 04 §3.2); every other bindable value is JSON already. */
91
+ function encodeParam(value) {
92
+ if (value instanceof ArrayBuffer)
93
+ return { $bytes: toBase64(new Uint8Array(value)) };
94
+ if (ArrayBuffer.isView(value))
95
+ return { $bytes: toBase64(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)) };
96
+ return value;
97
+ }
98
+ function toBase64(bytes) {
99
+ let binary = '';
100
+ for (const b of bytes)
101
+ binary += String.fromCharCode(b);
102
+ return btoa(binary);
103
+ }
@@ -0,0 +1,10 @@
1
+ import type { D1Database, TotalumD1Options } from './types.js';
2
+ export { TotalumD1Error, isTotalumD1Error } from './errors.js';
3
+ export type { TotalumD1ClientCode, TotalumD1ErrorCode } from './errors.js';
4
+ export type { D1Database, D1DatabaseSession, D1ExecResult, D1PreparedStatement, D1Result, TotalumD1Options, } from './types.js';
5
+ /**
6
+ * A `D1Database` for the project's own database wherever the code runs (A3.4, plan 05 §3): the bound
7
+ * `TOTALUM_DB` inside a Worker, the libsql adapter when the environment moved to Turso, and an HTTPS client to
8
+ * SDK-API everywhere else. Detection happens on the first statement, never at call time (§3.1).
9
+ */
10
+ export declare function totalumD1(options?: TotalumD1Options): D1Database;
@@ -0,0 +1,61 @@
1
+ import { TotalumD1Error } from './errors.js';
2
+ import { httpsExecutor } from './https.js';
3
+ import { lazyD1 } from './lazy.js';
4
+ import { libsqlExecutor } from './libsql.js';
5
+ export { TotalumD1Error, isTotalumD1Error } from './errors.js';
6
+ const DEFAULT_API_URL = 'https://sdk-api.totalum.app';
7
+ /**
8
+ * A `D1Database` for the project's own database wherever the code runs (A3.4, plan 05 §3): the bound
9
+ * `TOTALUM_DB` inside a Worker, the libsql adapter when the environment moved to Turso, and an HTTPS client to
10
+ * SDK-API everywhere else. Detection happens on the first statement, never at call time (§3.1).
11
+ */
12
+ export function totalumD1(options = {}) {
13
+ return lazyD1(() => resolveTarget(options), options.mode === 'migrate');
14
+ }
15
+ async function resolveTarget(options) {
16
+ if (options.binding)
17
+ return options.binding;
18
+ const inWorker = typeof navigator !== 'undefined' && navigator.userAgent === 'Cloudflare-Workers';
19
+ if (inWorker && options.transport !== 'https') {
20
+ if (readEnv('TOTALUM_DB_PROVIDER') === 'turso')
21
+ return await tursoTarget();
22
+ const binding = await openNextBinding();
23
+ if (binding)
24
+ return binding;
25
+ }
26
+ const key = options.key ?? readEnv('TOTALUM_PROJECT_KEY');
27
+ if (key === undefined) {
28
+ throw TotalumD1Error.client('SDK_NOT_CONFIGURED', 'Set TOTALUM_PROJECT_KEY (server-side) or pass { binding } to totalumD1().');
29
+ }
30
+ return httpsExecutor({
31
+ apiUrl: options.apiUrl ?? readEnv('TOTALUM_SDK_API_URL') ?? DEFAULT_API_URL,
32
+ key,
33
+ env: options.env ?? readEnv('TOTALUM_ENV') ?? 'live',
34
+ migrate: options.mode === 'migrate',
35
+ destructive: options.destructive === true,
36
+ });
37
+ }
38
+ /** Env is `process.env` (Node; workerd with `nodejs_compat`, where OpenNext and Miniflare populate it from bindings). */
39
+ function readEnv(name) {
40
+ const value = typeof process === 'undefined' ? undefined : process.env[name];
41
+ return value === '' ? undefined : value;
42
+ }
43
+ /** OpenNext exposes the request's bindings; absent (a laptop) or outside a request, there is no binding. */
44
+ async function openNextBinding() {
45
+ try {
46
+ const openNext = (await import('@opennextjs/cloudflare'));
47
+ return openNext.getCloudflareContext().env['TOTALUM_DB'];
48
+ }
49
+ catch {
50
+ return undefined;
51
+ }
52
+ }
53
+ async function tursoTarget() {
54
+ const url = readEnv('TURSO_DATABASE_URL');
55
+ const authToken = readEnv('TURSO_AUTH_TOKEN');
56
+ if (url === undefined || authToken === undefined) {
57
+ throw TotalumD1Error.client('SDK_NOT_CONFIGURED', 'TOTALUM_DB_PROVIDER=turso requires TURSO_DATABASE_URL and TURSO_AUTH_TOKEN');
58
+ }
59
+ const web = (await import('@libsql/client/web'));
60
+ return libsqlExecutor(web.createClient({ url, authToken, intMode: 'bigint' }));
61
+ }
@@ -0,0 +1,7 @@
1
+ import type { D1Database, Executor } from './types.js';
2
+ /**
3
+ * The object `totalumD1()` hands out: no I/O and no environment detection until the first statement runs;
4
+ * the target is resolved once and cached (plan 05 §3.1). Statements remember `sql` + params and are
5
+ * materialised on the resolved target when executed, so `drizzle(totalumD1())` at module scope is legal.
6
+ */
7
+ export declare function lazyD1(resolve: () => Promise<Executor>, migrate: boolean): D1Database;
@@ -0,0 +1,72 @@
1
+ import { TotalumD1Error } from './errors.js';
2
+ import { splitStatements } from './sql.js';
3
+ /**
4
+ * The object `totalumD1()` hands out: no I/O and no environment detection until the first statement runs;
5
+ * the target is resolved once and cached (plan 05 §3.1). Statements remember `sql` + params and are
6
+ * materialised on the resolved target when executed, so `drizzle(totalumD1())` at module scope is legal.
7
+ */
8
+ export function lazyD1(resolve, migrate) {
9
+ let target;
10
+ // a failed resolution is not cached, so a transient failure does not poison the module-scope instance
11
+ const resolved = () => (target ??= resolve().catch((e) => {
12
+ target = undefined;
13
+ throw e;
14
+ }));
15
+ class LazyStatement {
16
+ sql;
17
+ params;
18
+ constructor(sql, params = []) {
19
+ this.sql = sql;
20
+ this.params = params;
21
+ }
22
+ bind(...values) {
23
+ return new LazyStatement(this.sql, values);
24
+ }
25
+ async real() {
26
+ return (await resolved()).prepare(this.sql).bind(...this.params);
27
+ }
28
+ async first(colName) {
29
+ return (await this.real()).first(colName);
30
+ }
31
+ async run() {
32
+ return (await this.real()).run();
33
+ }
34
+ async all() {
35
+ return (await this.real()).all();
36
+ }
37
+ async raw(options) {
38
+ const s = await this.real();
39
+ return options?.columnNames === true ? s.raw({ columnNames: true }) : s.raw();
40
+ }
41
+ }
42
+ const batch = async (statements) => {
43
+ const t = await resolved();
44
+ const real = statements.map((s) => {
45
+ if (!(s instanceof LazyStatement)) {
46
+ throw TotalumD1Error.client('BATCH_FOREIGN_STATEMENT', 'batch() only accepts statements prepared by this totalumD1() instance');
47
+ }
48
+ return t.prepare(s.sql).bind(...s.params);
49
+ });
50
+ return t.batch(real);
51
+ };
52
+ const exec = async (script) => {
53
+ if (!migrate) {
54
+ throw TotalumD1Error.client('EXEC_REQUIRES_MIGRATE_MODE', 'exec() runs DDL scripts: use totalumD1({ mode: "migrate" }) from scripts/migrate.ts, or db.batch([...]) for data');
55
+ }
56
+ const t = await resolved();
57
+ if (t.exec)
58
+ return t.exec(script);
59
+ const statements = splitStatements(script);
60
+ const results = await t.batch(statements.map((sql) => t.prepare(sql)));
61
+ return { count: statements.length, duration: results.reduce((sum, r) => sum + r.meta.duration, 0) };
62
+ };
63
+ return {
64
+ prepare: (sql) => new LazyStatement(sql),
65
+ batch,
66
+ exec,
67
+ dump: () => Promise.reject(TotalumD1Error.client('NOT_SUPPORTED', 'dump() is not available; use the platform backups')),
68
+ withSession: () => {
69
+ throw TotalumD1Error.client('NOT_SUPPORTED', 'withSession() is not available in this SDK version');
70
+ },
71
+ };
72
+ }
@@ -0,0 +1,23 @@
1
+ import type { Executor } from './types.js';
2
+ /** The slice of `@libsql/client`'s `Client` the adapter uses, declared structurally (no third-party types leak). */
3
+ export interface LibsqlClient {
4
+ execute(statement: LibsqlStatement): Promise<LibsqlResultSet>;
5
+ batch(statements: LibsqlStatement[], mode: 'write'): Promise<LibsqlResultSet[]>;
6
+ }
7
+ export type LibsqlValue = string | number | bigint | boolean | null | ArrayBuffer | Uint8Array;
8
+ export interface LibsqlStatement {
9
+ sql: string;
10
+ args: LibsqlValue[];
11
+ }
12
+ export interface LibsqlResultSet {
13
+ columns: string[];
14
+ rows: ArrayLike<unknown>[];
15
+ rowsAffected: number;
16
+ lastInsertRowid: bigint | undefined | null;
17
+ }
18
+ /**
19
+ * The Turso path (A3.8, plan 05 §3.10): a `D1Database`-compatible executor over a libsql client created with
20
+ * `intMode: 'bigint'` (integers beyond 2^53 then degrade to the same double D1 returns instead of throwing).
21
+ * `meta` is synthesised from what libsql reports; `size_after` is not reported by libsql and is 0.
22
+ */
23
+ export declare function libsqlExecutor(client: LibsqlClient): Executor;
@@ -0,0 +1,92 @@
1
+ import { TotalumD1Error } from './errors.js';
2
+ import { assertBindable, pickFirst } from './sql.js';
3
+ /**
4
+ * The Turso path (A3.8, plan 05 §3.10): a `D1Database`-compatible executor over a libsql client created with
5
+ * `intMode: 'bigint'` (integers beyond 2^53 then degrade to the same double D1 returns instead of throwing).
6
+ * `meta` is synthesised from what libsql reports; `size_after` is not reported by libsql and is 0.
7
+ */
8
+ export function libsqlExecutor(client) {
9
+ let lastRowId = 0;
10
+ const toResult = (rs) => {
11
+ if (rs.lastInsertRowid !== undefined && rs.lastInsertRowid !== null)
12
+ lastRowId = Number(rs.lastInsertRowid);
13
+ const rows = rs.rows.map((row) => Array.from(row, fromLibsqlValue));
14
+ const meta = {
15
+ duration: 0,
16
+ changes: rs.rowsAffected,
17
+ last_row_id: lastRowId,
18
+ rows_read: rows.length,
19
+ rows_written: rs.rowsAffected,
20
+ changed_db: rs.rowsAffected > 0,
21
+ size_after: 0,
22
+ };
23
+ const results = rows.map((row) => Object.fromEntries(row.map((cell, i) => [rs.columns[i], cell])));
24
+ return { results, success: true, meta };
25
+ };
26
+ const run = async (f) => {
27
+ try {
28
+ return await f();
29
+ }
30
+ catch (e) {
31
+ throw mapLibsqlError(e);
32
+ }
33
+ };
34
+ class LibsqlD1Statement {
35
+ sql;
36
+ args;
37
+ constructor(sql, args = []) {
38
+ this.sql = sql;
39
+ this.args = args;
40
+ }
41
+ bind(...values) {
42
+ assertBindable(values);
43
+ return new LibsqlD1Statement(this.sql, values);
44
+ }
45
+ execute() {
46
+ return run(() => client.execute({ sql: this.sql, args: this.args }));
47
+ }
48
+ async first(colName) {
49
+ return pickFirst(toResult(await this.execute()).results, colName);
50
+ }
51
+ async run() {
52
+ return toResult(await this.execute());
53
+ }
54
+ all() {
55
+ return this.run();
56
+ }
57
+ async raw(options) {
58
+ const rs = await this.execute();
59
+ toResult(rs);
60
+ const rows = rs.rows.map((row) => Array.from(row, fromLibsqlValue));
61
+ return options?.columnNames === true ? [rs.columns, ...rows] : rows;
62
+ }
63
+ }
64
+ return {
65
+ prepare: (sql) => new LibsqlD1Statement(sql),
66
+ async batch(statements) {
67
+ const stmts = statements.map((s) => ({
68
+ sql: s.sql,
69
+ args: s.args,
70
+ }));
71
+ return (await run(() => client.batch(stmts, 'write'))).map((rs) => toResult(rs));
72
+ },
73
+ };
74
+ }
75
+ function fromLibsqlValue(v) {
76
+ if (typeof v === 'bigint')
77
+ return Number(v);
78
+ if (v instanceof ArrayBuffer)
79
+ return Array.from(new Uint8Array(v));
80
+ return v;
81
+ }
82
+ /** `LibsqlError { code, message: '<CODE>: <sqlite message>' }` → the `D1_ERROR: <sqlite message>: <CODE>` shape. */
83
+ function mapLibsqlError(e) {
84
+ const err = e;
85
+ if (typeof err.code !== 'string' || !err.code.startsWith('SQLITE_') || typeof err.message !== 'string')
86
+ return e;
87
+ const sqliteMessage = `${err.message.replace(/^(SQLITE_\w+: )+/, '')}: ${err.code}`;
88
+ return new TotalumD1Error('SQL_ERROR', `D1_ERROR: ${sqliteMessage}`, {
89
+ status: 400,
90
+ details: { sqliteMessage },
91
+ });
92
+ }
@@ -0,0 +1,9 @@
1
+ /** Native D1 refuses these at `bind()` time with `D1_TYPE_ERROR`; both non-native paths do the same, synchronously. */
2
+ export declare function assertBindable(values: unknown[]): void;
3
+ /** `first(col)` as the binding does it: `null` on no rows, `D1_COLUMN_NOTFOUND` on an unknown column. */
4
+ export declare function pickFirst(results: unknown[], colName: string | undefined): unknown;
5
+ /**
6
+ * Splits an `exec()` script on statement boundaries — `;` inside string literals, quoted identifiers and
7
+ * comments is not a boundary (plan 05 §3.4). Empty trailing fragments are dropped.
8
+ */
9
+ export declare function splitStatements(script: string): string[];
package/dist/d1/sql.js ADDED
@@ -0,0 +1,87 @@
1
+ import { isRecord } from './errors.js';
2
+ /** Native D1 refuses these at `bind()` time with `D1_TYPE_ERROR`; both non-native paths do the same, synchronously. */
3
+ export function assertBindable(values) {
4
+ for (const v of values) {
5
+ const ok = v === null ||
6
+ typeof v === 'string' ||
7
+ typeof v === 'number' ||
8
+ typeof v === 'boolean' ||
9
+ v instanceof ArrayBuffer ||
10
+ ArrayBuffer.isView(v);
11
+ if (!ok)
12
+ throw new Error(`D1_TYPE_ERROR: Type '${typeof v}' not supported for value '${describe(v)}'`);
13
+ }
14
+ }
15
+ /** The value as D1 prints it in `D1_TYPE_ERROR` (`${value}`). */
16
+ function describe(v) {
17
+ switch (typeof v) {
18
+ case 'bigint':
19
+ case 'number':
20
+ case 'boolean':
21
+ case 'string':
22
+ case 'symbol':
23
+ return v.toString();
24
+ case 'undefined':
25
+ return 'undefined';
26
+ case 'function':
27
+ return 'function';
28
+ default:
29
+ return '[object Object]';
30
+ }
31
+ }
32
+ /** `first(col)` as the binding does it: `null` on no rows, `D1_COLUMN_NOTFOUND` on an unknown column. */
33
+ export function pickFirst(results, colName) {
34
+ const row = results[0];
35
+ if (row === undefined)
36
+ return null;
37
+ if (colName === undefined)
38
+ return row;
39
+ if (!isRecord(row) || !(colName in row))
40
+ throw new Error(`D1_COLUMN_NOTFOUND: Column not found (${colName})`);
41
+ return row[colName];
42
+ }
43
+ /**
44
+ * Splits an `exec()` script on statement boundaries — `;` inside string literals, quoted identifiers and
45
+ * comments is not a boundary (plan 05 §3.4). Empty trailing fragments are dropped.
46
+ */
47
+ export function splitStatements(script) {
48
+ const out = [];
49
+ let start = 0;
50
+ let i = 0;
51
+ while (i < script.length) {
52
+ const c = script[i];
53
+ const next = script[i + 1];
54
+ if (c === "'" || c === '"' || c === '`' || c === '[') {
55
+ const close = c === '[' ? ']' : c;
56
+ i = script.indexOf(close, i + 1);
57
+ if (i === -1)
58
+ break;
59
+ // a doubled quote is an escaped quote inside the literal
60
+ while (close !== ']' && script[i + 1] === close)
61
+ i = script.indexOf(close, i + 2);
62
+ if (i === -1)
63
+ break;
64
+ i++;
65
+ }
66
+ else if (c === '-' && next === '-') {
67
+ i = script.indexOf('\n', i);
68
+ if (i === -1)
69
+ break;
70
+ }
71
+ else if (c === '/' && next === '*') {
72
+ i = script.indexOf('*/', i + 2);
73
+ if (i === -1)
74
+ break;
75
+ i += 2;
76
+ }
77
+ else if (c === ';') {
78
+ out.push(script.slice(start, i));
79
+ start = ++i;
80
+ }
81
+ else {
82
+ i++;
83
+ }
84
+ }
85
+ out.push(script.slice(start));
86
+ return out.map((s) => s.trim()).filter((s) => s.length > 0);
87
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Cloudflare's `D1Database` surface, re-declared structurally so an app needs no Cloudflare types to compile
3
+ * and `drizzle-orm/d1` accepts the object (plan 05 §1, §3.1). `D1Meta` is Cloudflare's own interface
4
+ * (`size_after` required); the contracts wire shape is looser and every value here satisfies it (type-tested).
5
+ */
6
+ export type D1Meta = {
7
+ duration: number;
8
+ size_after: number;
9
+ rows_read: number;
10
+ rows_written: number;
11
+ last_row_id: number;
12
+ changed_db: boolean;
13
+ changes: number;
14
+ served_by_region?: string;
15
+ served_by_primary?: boolean;
16
+ };
17
+ export interface D1Result<T = Record<string, unknown>> {
18
+ results: T[];
19
+ success: true;
20
+ meta: D1Meta;
21
+ }
22
+ export interface D1ExecResult {
23
+ count: number;
24
+ duration: number;
25
+ }
26
+ export interface D1PreparedStatement {
27
+ bind(...values: unknown[]): D1PreparedStatement;
28
+ first<T = Record<string, unknown>>(colName?: string): Promise<T | null>;
29
+ run<T = Record<string, unknown>>(): Promise<D1Result<T>>;
30
+ all<T = Record<string, unknown>>(): Promise<D1Result<T>>;
31
+ raw<T = unknown[]>(options: {
32
+ columnNames: true;
33
+ }): Promise<[string[], ...T[]]>;
34
+ raw<T = unknown[]>(options?: {
35
+ columnNames?: false;
36
+ }): Promise<T[]>;
37
+ }
38
+ export interface D1DatabaseSession {
39
+ prepare(sql: string): D1PreparedStatement;
40
+ batch<T = unknown>(statements: D1PreparedStatement[]): Promise<D1Result<T>[]>;
41
+ getBookmark(): string | null;
42
+ }
43
+ export interface D1Database {
44
+ prepare(sql: string): D1PreparedStatement;
45
+ batch<T = unknown>(statements: D1PreparedStatement[]): Promise<D1Result<T>[]>;
46
+ exec(sql: string): Promise<D1ExecResult>;
47
+ dump(): Promise<ArrayBuffer>;
48
+ withSession(constraintOrBookmark?: string): D1DatabaseSession;
49
+ }
50
+ /** What a resolved target must offer; a native binding satisfies it, the HTTPS and libsql executors implement it. */
51
+ export interface Executor {
52
+ prepare(sql: string): D1PreparedStatement;
53
+ batch<T = unknown>(statements: D1PreparedStatement[]): Promise<D1Result<T>[]>;
54
+ /** Native only: the binding's own line-based `exec`. Executors without it run `exec` as one atomic batch. */
55
+ exec?(sql: string): Promise<D1ExecResult>;
56
+ }
57
+ export interface TotalumD1Options {
58
+ /** `'migrate'` (scripts/migrate.ts only) sends `x-totalum-migrate: 1` and enables `exec()`; default `'query'`. */
59
+ mode?: 'query' | 'migrate';
60
+ /** Migrate mode only: sends `x-totalum-migrate-destructive: 1` (A10.4). */
61
+ destructive?: boolean;
62
+ /** HTTPS path only; default `TOTALUM_ENV`, then `'live'`. */
63
+ env?: 'dev' | 'live';
64
+ /** Default `TOTALUM_SDK_API_URL`, then `https://sdk-api.totalum.app`. */
65
+ apiUrl?: string;
66
+ /** Default `TOTALUM_PROJECT_KEY`. */
67
+ key?: string;
68
+ /** An explicit binding wins over detection (plain Workers: `env.TOTALUM_DB`; tests: a Miniflare D1). */
69
+ binding?: D1Database;
70
+ /** `'https'` forces the HTTPS path even inside a Worker (the conformance corpus); default `'auto'`. */
71
+ transport?: 'auto' | 'https';
72
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "totalum-sdk",
3
+ "version": "0.1.0-dev.2",
4
+ "description": "The SDK generated Totalum apps import: totalum-sdk/d1 (D1 driver — native binding in a Worker, HTTPS to SDK-API elsewhere)",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "packageManager": "pnpm@10.34.5",
8
+ "engines": {
9
+ "node": ">=22 <23"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/francesc-commits/totalum-sdk.git"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "exports": {
19
+ "./d1": {
20
+ "types": "./dist/d1/index.d.ts",
21
+ "default": "./dist/d1/index.js"
22
+ }
23
+ },
24
+ "sideEffects": false,
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.build.json",
27
+ "typecheck": "tsc --noEmit",
28
+ "lint": "eslint . && prettier --check .",
29
+ "format": "prettier --write .",
30
+ "test": "vitest run",
31
+ "test:node": "vitest run --project node",
32
+ "test:workerd": "vitest run --project workerd",
33
+ "prepare": "lefthook install || echo \"lefthook: hooks not installed (git >= 2.31 required)\""
34
+ },
35
+ "peerDependencies": {
36
+ "@libsql/client": "0.18.0"
37
+ },
38
+ "peerDependenciesMeta": {
39
+ "@libsql/client": {
40
+ "optional": true
41
+ }
42
+ },
43
+ "devDependencies": {
44
+ "@cloudflare/vitest-pool-workers": "0.22.0",
45
+ "@cloudflare/workers-types": "5.20260922.1",
46
+ "@commitlint/cli": "21.2.3",
47
+ "@commitlint/config-conventional": "21.2.3",
48
+ "@eslint/js": "9.39.5",
49
+ "@libsql/client": "0.18.0",
50
+ "@speed-paradigm/totalum-contracts": "link:../totalum-contracts",
51
+ "@types/node": "22.20.4",
52
+ "drizzle-orm": "0.45.3",
53
+ "esbuild": "0.28.1",
54
+ "eslint": "9.39.5",
55
+ "eslint-plugin-import-x": "4.17.1",
56
+ "eslint-plugin-no-only-tests": "3.4.0",
57
+ "lefthook": "2.1.14",
58
+ "miniflare": "5.20260921.0-alpha",
59
+ "prettier": "3.9.8",
60
+ "totalum-sdk-api": "link:../totalum-sdk-api",
61
+ "typescript": "5.8.3",
62
+ "typescript-eslint": "8.70.1",
63
+ "vitest": "4.1.11",
64
+ "wrangler": "4.136.2"
65
+ },
66
+ "prettier": "@speed-paradigm/totalum-contracts/configs/prettier.json"
67
+ }