@pikku/kysely-sqlite 0.12.13 → 0.12.14
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/CHANGELOG.md +29 -0
- package/package.json +11 -7
- package/run-tests.sh +59 -24
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/create-sqlite-kysely.ts +0 -12
- package/src/index.ts +0 -18
- package/src/libsql-web-dialect.test.ts +0 -151
- package/src/libsql-web-dialect.ts +0 -282
- package/src/sqlite-functions.ts +0 -47
- package/src/sqlite-kysely-agent-run-service.ts +0 -3
- package/src/sqlite-kysely-agent-storage-service.ts +0 -3
- package/src/sqlite-kysely-channel-store.ts +0 -3
- package/src/sqlite-kysely-deployment-service.ts +0 -3
- package/src/sqlite-kysely-eventhub-store.ts +0 -3
- package/src/sqlite-kysely-secret-service.ts +0 -3
- package/src/sqlite-kysely-workflow-run-service.ts +0 -3
- package/src/sqlite-kysely-workflow-service.ts +0 -3
- package/tsconfig.json +0 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# @pikku/kysely-sqlite
|
|
2
2
|
|
|
3
|
+
## 0.12.14
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 4c7a1b5: Run the monorepo's own scripts through bun instead of yarn. What moves is the
|
|
8
|
+
package manager each package's `prepublishOnly` and build scripts invoke, plus
|
|
9
|
+
the two manifest fixes bun needs to resolve the tree: `uWebSockets.js` is
|
|
10
|
+
declared with an explicit `github:` specifier, and `@pikku/uws-handler` marks
|
|
11
|
+
its `uWebSockets.js` peer optional so a bun install of a consumer that brings
|
|
12
|
+
its own uWS app does not try to fetch it from the registry.
|
|
13
|
+
|
|
14
|
+
Three published behaviours change, all of them cases where an isolated
|
|
15
|
+
`node_modules` or bun as the runtime had been papered over by yarn's hoisting:
|
|
16
|
+
|
|
17
|
+
- `@pikku/migrator-sql` turns foreign keys on when it opens a sqlite database
|
|
18
|
+
through bun. `node:sqlite` enforces them by default and `bun:sqlite` does not,
|
|
19
|
+
which silently turned every `ON DELETE CASCADE` into a no-op under
|
|
20
|
+
`bunx --bun pikku`.
|
|
21
|
+
- `@pikku/cli` resolves a deploy provider against the project being deployed
|
|
22
|
+
rather than against wherever the CLI itself is installed, which is what its
|
|
23
|
+
own "is not installed" error asks the user to arrange.
|
|
24
|
+
- `@pikku/cli` treats a specifier a runtime hands straight back — bun does this
|
|
25
|
+
for the modules it implements itself — as not resolved from the project, so
|
|
26
|
+
it falls back rather than loading the runtime's own copy.
|
|
27
|
+
|
|
28
|
+
- Updated dependencies [4c7a1b5]
|
|
29
|
+
- @pikku/core@0.12.108
|
|
30
|
+
- @pikku/kysely@0.13.25
|
|
31
|
+
|
|
3
32
|
## 0.12.13
|
|
4
33
|
|
|
5
34
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/kysely-sqlite",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.14",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/pikkujs/pikku.git",
|
|
7
|
+
"directory": "packages/services/kysely-sqlite"
|
|
8
|
+
},
|
|
4
9
|
"description": "SQLite dialect services and libSQL web dialect for Pikku, backed by Kysely",
|
|
5
10
|
"author": "yasser.fadl@gmail.com",
|
|
6
11
|
"license": "MIT",
|
|
@@ -14,20 +19,19 @@
|
|
|
14
19
|
"test": "bash run-tests.sh",
|
|
15
20
|
"test:watch": "bash run-tests.sh --watch",
|
|
16
21
|
"test:coverage": "bash run-tests.sh --coverage",
|
|
17
|
-
"prepublishOnly": "
|
|
22
|
+
"prepublishOnly": "bun run build"
|
|
18
23
|
},
|
|
19
24
|
"dependencies": {
|
|
20
|
-
"@pikku/kysely": "^0.13.
|
|
25
|
+
"@pikku/kysely": "^0.13.25",
|
|
21
26
|
"kysely": "^0.29.5"
|
|
22
27
|
},
|
|
23
28
|
"peerDependencies": {
|
|
24
|
-
"@pikku/core": "^0.12.
|
|
29
|
+
"@pikku/core": "^0.12.108"
|
|
25
30
|
},
|
|
26
31
|
"devDependencies": {
|
|
27
|
-
"@pikku/core": "^0.12.
|
|
28
|
-
"typescript": "^6.0.3"
|
|
32
|
+
"@pikku/core": "^0.12.108"
|
|
29
33
|
},
|
|
30
34
|
"exports": {
|
|
31
35
|
".": "./dist/src/index.js"
|
|
32
36
|
}
|
|
33
|
-
}
|
|
37
|
+
}
|
package/run-tests.sh
CHANGED
|
@@ -1,33 +1,68 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
3
|
+
# Enable nullglob to handle cases where no files match the pattern
|
|
4
|
+
shopt -s nullglob
|
|
5
|
+
|
|
6
|
+
# Initialize variables for options
|
|
7
|
+
watch_mode=false
|
|
8
|
+
coverage_mode=false
|
|
9
|
+
|
|
10
|
+
# Parse command-line options
|
|
11
|
+
while [[ $# -gt 0 ]]; do
|
|
12
|
+
case $1 in
|
|
13
|
+
--watch)
|
|
14
|
+
watch_mode=true
|
|
15
|
+
shift
|
|
16
|
+
;;
|
|
17
|
+
--coverage)
|
|
18
|
+
coverage_mode=true
|
|
19
|
+
shift
|
|
20
|
+
;;
|
|
21
|
+
*)
|
|
22
|
+
echo "Unknown option: $1"
|
|
23
|
+
exit 1
|
|
24
|
+
;;
|
|
25
|
+
esac
|
|
19
26
|
done
|
|
20
27
|
|
|
21
|
-
#
|
|
22
|
-
|
|
28
|
+
# Define the pattern to match your test files
|
|
29
|
+
pattern="src/*.test.ts"
|
|
30
|
+
|
|
31
|
+
# Expand the pattern into an array of files
|
|
32
|
+
files=($(find src -type f -name "*.test.ts"))
|
|
33
|
+
|
|
34
|
+
# Check if any files matched the pattern
|
|
35
|
+
if [ ${#files[@]} -eq 0 ]; then
|
|
36
|
+
echo "No test files found matching pattern: $pattern"
|
|
37
|
+
if [ "${STRICT_TEST_DISCOVERY}" = "1" ]; then
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
exit 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# Construct the bun command
|
|
44
|
+
#
|
|
45
|
+
# --parallel gives every test file its own worker process, which is what node's
|
|
46
|
+
# `--test` did. Several suites keep module-level caches (a TypeScript program, a
|
|
47
|
+
# temp fixture directory) that a shared process would leak between files.
|
|
48
|
+
#
|
|
49
|
+
# --timeout because bun caps a test at 5s by default where node had no cap, and
|
|
50
|
+
# the suites that build real TypeScript programs run well past that.
|
|
51
|
+
bun_cmd=(bun test --parallel --timeout 120000)
|
|
23
52
|
|
|
24
|
-
|
|
25
|
-
|
|
53
|
+
# Append options based on flags
|
|
54
|
+
if [ "$watch_mode" = true ]; then
|
|
55
|
+
# --watch reruns in one process; parallel workers have nothing to watch.
|
|
56
|
+
bun_cmd=(bun test --watch --timeout 120000)
|
|
26
57
|
fi
|
|
27
58
|
|
|
28
|
-
if [ "$
|
|
29
|
-
|
|
59
|
+
if [ "$coverage_mode" = true ]; then
|
|
60
|
+
# --coverage-dir=. keeps lcov.info at the package root: CI merges every
|
|
61
|
+
# package's report by prefixing its SF paths with the file's own directory,
|
|
62
|
+
# so a nested coverage/ directory would re-root them one level too deep.
|
|
63
|
+
bun_cmd+=(--coverage --coverage-reporter=lcov --coverage-reporter=text --coverage-dir=.)
|
|
64
|
+
export PIKKU_TEST_COVERAGE=1
|
|
30
65
|
fi
|
|
31
66
|
|
|
32
|
-
# Execute the command
|
|
33
|
-
|
|
67
|
+
# Execute the bun command with the expanded list of files
|
|
68
|
+
"${bun_cmd[@]}" "${files[@]}"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"root":["../src/create-sqlite-kysely.ts","../src/index.ts","../src/libsql-web-dialect.ts","../src/sqlite-functions.ts","../src/sqlite-kysely-agent-run-service.ts","../src/sqlite-kysely-agent-storage-service.ts","../src/sqlite-kysely-channel-store.ts","../src/sqlite-kysely-deployment-service.ts","../src/sqlite-kysely-eventhub-store.ts","../src/sqlite-kysely-secret-service.ts","../src/sqlite-kysely-workflow-run-service.ts","../src/sqlite-kysely-workflow-service.ts"],"version":"6.0.3"}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { Kysely, SqliteDialect, type SqliteDatabase } from 'kysely'
|
|
2
|
-
import { SerializePlugin } from '@pikku/kysely'
|
|
3
|
-
import type { KyselyPikkuDB } from '@pikku/kysely'
|
|
4
|
-
|
|
5
|
-
export function createSQLiteKysely(
|
|
6
|
-
database: SqliteDatabase | (() => Promise<SqliteDatabase>)
|
|
7
|
-
): Kysely<KyselyPikkuDB> {
|
|
8
|
-
return new Kysely<KyselyPikkuDB>({
|
|
9
|
-
dialect: new SqliteDialect({ database }),
|
|
10
|
-
plugins: [new SerializePlugin()],
|
|
11
|
-
})
|
|
12
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export { createSQLiteKysely } from './create-sqlite-kysely.js'
|
|
2
|
-
export { SQLiteKyselyWorkflowService } from './sqlite-kysely-workflow-service.js'
|
|
3
|
-
export { SQLiteKyselyDeploymentService } from './sqlite-kysely-deployment-service.js'
|
|
4
|
-
export { SQLiteKyselyAgentStorageService } from './sqlite-kysely-agent-storage-service.js'
|
|
5
|
-
export { SQLiteKyselyAgentRunService } from './sqlite-kysely-agent-run-service.js'
|
|
6
|
-
export { SQLiteKyselyWorkflowRunService } from './sqlite-kysely-workflow-run-service.js'
|
|
7
|
-
export { SQLiteKyselyChannelStore } from './sqlite-kysely-channel-store.js'
|
|
8
|
-
export { SQLiteKyselyEventHubStore } from './sqlite-kysely-eventhub-store.js'
|
|
9
|
-
export { SQLiteKyselySecretService } from './sqlite-kysely-secret-service.js'
|
|
10
|
-
export {
|
|
11
|
-
SqliteFunctionsUnsupportedError,
|
|
12
|
-
type SqliteFunctionMap,
|
|
13
|
-
} from './sqlite-functions.js'
|
|
14
|
-
export { LibsqlWebDialect } from './libsql-web-dialect.js'
|
|
15
|
-
|
|
16
|
-
export type { KyselyPikkuDB } from '@pikku/kysely'
|
|
17
|
-
export type { WorkflowRunService } from '@pikku/core/workflow'
|
|
18
|
-
export type { AgentRunService, AgentRunRow } from '@pikku/core/agent'
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { describe, test, beforeEach, afterEach } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { Kysely } from 'kysely'
|
|
4
|
-
import { LibsqlWebDialect } from './libsql-web-dialect.js'
|
|
5
|
-
|
|
6
|
-
interface TestDB {
|
|
7
|
-
entry: {
|
|
8
|
-
id: string
|
|
9
|
-
createdAt: string
|
|
10
|
-
payload: string
|
|
11
|
-
archived: number
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Args of the last `execute` request the dialect put on the wire. */
|
|
16
|
-
let sentArgs: Array<Record<string, unknown>>
|
|
17
|
-
let realFetch: typeof globalThis.fetch
|
|
18
|
-
|
|
19
|
-
const okResponse = () =>
|
|
20
|
-
new Response(
|
|
21
|
-
JSON.stringify({
|
|
22
|
-
baton: null,
|
|
23
|
-
base_url: null,
|
|
24
|
-
results: [
|
|
25
|
-
{
|
|
26
|
-
type: 'ok',
|
|
27
|
-
response: {
|
|
28
|
-
type: 'execute',
|
|
29
|
-
result: {
|
|
30
|
-
cols: [],
|
|
31
|
-
rows: [],
|
|
32
|
-
affected_row_count: 1,
|
|
33
|
-
last_insert_rowid: '1',
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
],
|
|
38
|
-
}),
|
|
39
|
-
{ status: 200, headers: { 'content-type': 'application/json' } }
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
const db = () =>
|
|
43
|
-
new Kysely<TestDB>({
|
|
44
|
-
dialect: new LibsqlWebDialect({ url: 'https://example.turso.io' }),
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
describe('LibsqlWebDialect bind-parameter encoding', () => {
|
|
48
|
-
beforeEach(() => {
|
|
49
|
-
sentArgs = []
|
|
50
|
-
realFetch = globalThis.fetch
|
|
51
|
-
globalThis.fetch = (async (_url: string, init: RequestInit) => {
|
|
52
|
-
const body = JSON.parse(init.body as string)
|
|
53
|
-
for (const req of body.requests) {
|
|
54
|
-
if (req.type === 'execute') sentArgs = req.stmt.args
|
|
55
|
-
}
|
|
56
|
-
return okResponse()
|
|
57
|
-
}) as unknown as typeof globalThis.fetch
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
afterEach(() => {
|
|
61
|
-
globalThis.fetch = realFetch
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
test('encodes a Date as an ISO-8601 text value', async () => {
|
|
65
|
-
const createdAt = new Date('2026-07-29T21:00:00.000Z')
|
|
66
|
-
await db()
|
|
67
|
-
.insertInto('entry')
|
|
68
|
-
.values({ id: 'a', createdAt: createdAt as never } as never)
|
|
69
|
-
.execute()
|
|
70
|
-
|
|
71
|
-
assert.deepEqual(sentArgs[1], {
|
|
72
|
-
type: 'text',
|
|
73
|
-
value: '2026-07-29T21:00:00.000Z',
|
|
74
|
-
})
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
test('encodes a plain object as JSON text', async () => {
|
|
78
|
-
await db()
|
|
79
|
-
.insertInto('entry')
|
|
80
|
-
.values({ id: 'a', payload: { tags: [1, 2] } as never } as never)
|
|
81
|
-
.execute()
|
|
82
|
-
|
|
83
|
-
assert.deepEqual(sentArgs[1], {
|
|
84
|
-
type: 'text',
|
|
85
|
-
value: '{"tags":[1,2]}',
|
|
86
|
-
})
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
test('encodes an array bound directly as JSON text', async () => {
|
|
90
|
-
await db()
|
|
91
|
-
.insertInto('entry')
|
|
92
|
-
.values({ id: 'a', payload: [1, 2] as never } as never)
|
|
93
|
-
.execute()
|
|
94
|
-
|
|
95
|
-
assert.deepEqual(sentArgs[1], { type: 'text', value: '[1,2]' })
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
test('encodes a null-prototype record as JSON text', async () => {
|
|
99
|
-
const payload = Object.assign(Object.create(null), { tags: [1] })
|
|
100
|
-
await db()
|
|
101
|
-
.insertInto('entry')
|
|
102
|
-
.values({ id: 'a', payload: payload as never } as never)
|
|
103
|
-
.execute()
|
|
104
|
-
|
|
105
|
-
assert.deepEqual(sentArgs[1], { type: 'text', value: '{"tags":[1]}' })
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
test('keeps the primitive encodings intact', async () => {
|
|
109
|
-
await db()
|
|
110
|
-
.insertInto('entry')
|
|
111
|
-
.values({ id: 'a', archived: true as never } as never)
|
|
112
|
-
.execute()
|
|
113
|
-
|
|
114
|
-
assert.deepEqual(sentArgs, [
|
|
115
|
-
{ type: 'text', value: 'a' },
|
|
116
|
-
{ type: 'integer', value: '1' },
|
|
117
|
-
])
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
test('still rejects a value it cannot represent', async () => {
|
|
121
|
-
await assert.rejects(
|
|
122
|
-
db()
|
|
123
|
-
.insertInto('entry')
|
|
124
|
-
.values({ id: 'a', payload: Symbol('nope') as never } as never)
|
|
125
|
-
.execute(),
|
|
126
|
-
/unsupported argument type symbol/
|
|
127
|
-
)
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
// A Map or a class instance stringifies to "{}" or to a partial view of
|
|
131
|
-
// itself, so it must fail loudly rather than persist an empty JSON blob.
|
|
132
|
-
test('rejects an object JSON cannot faithfully represent', async () => {
|
|
133
|
-
class Money {
|
|
134
|
-
constructor(public cents: number) {}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
for (const [value, expected] of [
|
|
138
|
-
[new Map([['a', 1]]), /unsupported argument type Map/],
|
|
139
|
-
[/abc/i, /unsupported argument type RegExp/],
|
|
140
|
-
[new Money(100), /unsupported argument type Money/],
|
|
141
|
-
] as const) {
|
|
142
|
-
await assert.rejects(
|
|
143
|
-
db()
|
|
144
|
-
.insertInto('entry')
|
|
145
|
-
.values({ id: 'a', payload: value as never } as never)
|
|
146
|
-
.execute(),
|
|
147
|
-
expected
|
|
148
|
-
)
|
|
149
|
-
}
|
|
150
|
-
})
|
|
151
|
-
})
|
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
SqliteAdapter,
|
|
3
|
-
SqliteIntrospector,
|
|
4
|
-
SqliteQueryCompiler,
|
|
5
|
-
type DatabaseConnection,
|
|
6
|
-
type Dialect,
|
|
7
|
-
type Driver,
|
|
8
|
-
type CompiledQuery,
|
|
9
|
-
type QueryResult,
|
|
10
|
-
type Kysely,
|
|
11
|
-
type DatabaseIntrospector,
|
|
12
|
-
type DialectAdapter,
|
|
13
|
-
type QueryCompiler,
|
|
14
|
-
} from 'kysely'
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Kysely dialect for libsql over the *HTTP pipeline* transport (v2/pipeline).
|
|
18
|
-
*
|
|
19
|
-
* Why this file exists at all:
|
|
20
|
-
* - @libsql/kysely-libsql imports @libsql/client (Node), which uses node:http.
|
|
21
|
-
* CF Workers nodejs_compat_v2 doesn't ship node:http -> upload error 10021.
|
|
22
|
-
* - @libsql/client/web imports both ./http.js and ./ws.js at module top, and
|
|
23
|
-
* ./ws.js pulls in `ws` which does `require('node:events')` at load time.
|
|
24
|
-
* CF Workers also doesn't ship node:events -> upload error 10021.
|
|
25
|
-
* - @libsql/client/http re-uses @libsql/hrana-client's index which still
|
|
26
|
-
* imports ws transitively for the same reason.
|
|
27
|
-
*
|
|
28
|
-
* So we talk to the libsql/Turso HTTP pipeline directly with fetch. Single
|
|
29
|
-
* endpoint, single round-trip per execute. Transactions use the `baton`
|
|
30
|
-
* mechanism: every response returns a baton, and resending it on the next
|
|
31
|
-
* request keeps the same server-side stream (== same SQL connection ==
|
|
32
|
-
* same transaction).
|
|
33
|
-
*
|
|
34
|
-
* Pipeline reference: https://github.com/tursodatabase/libsql/blob/main/docs/HTTP_V2_SPEC.md
|
|
35
|
-
*/
|
|
36
|
-
export interface LibsqlWebDialectConfig {
|
|
37
|
-
url: string
|
|
38
|
-
authToken?: string
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export class LibsqlWebDialect implements Dialect {
|
|
42
|
-
constructor(private readonly config: LibsqlWebDialectConfig) {}
|
|
43
|
-
createAdapter(): DialectAdapter {
|
|
44
|
-
return new SqliteAdapter()
|
|
45
|
-
}
|
|
46
|
-
createDriver(): Driver {
|
|
47
|
-
return new LibsqlWebDriver(this.config)
|
|
48
|
-
}
|
|
49
|
-
createQueryCompiler(): QueryCompiler {
|
|
50
|
-
return new SqliteQueryCompiler()
|
|
51
|
-
}
|
|
52
|
-
createIntrospector(db: Kysely<unknown>): DatabaseIntrospector {
|
|
53
|
-
return new SqliteIntrospector(db)
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
type HranaValue =
|
|
58
|
-
| { type: 'null' }
|
|
59
|
-
| { type: 'integer'; value: string }
|
|
60
|
-
| { type: 'float'; value: number }
|
|
61
|
-
| { type: 'text'; value: string }
|
|
62
|
-
| { type: 'blob'; base64: string }
|
|
63
|
-
|
|
64
|
-
interface HranaStmtResult {
|
|
65
|
-
cols: Array<{ name: string | null; decltype: string | null }>
|
|
66
|
-
rows: HranaValue[][]
|
|
67
|
-
affected_row_count: number
|
|
68
|
-
last_insert_rowid: string | null
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
interface PipelineResponse {
|
|
72
|
-
baton: string | null
|
|
73
|
-
base_url: string | null
|
|
74
|
-
results: Array<
|
|
75
|
-
| {
|
|
76
|
-
type: 'ok'
|
|
77
|
-
response:
|
|
78
|
-
{ type: 'execute'; result: HranaStmtResult } | { type: 'close' }
|
|
79
|
-
}
|
|
80
|
-
| { type: 'error'; error: { message: string; code?: string } }
|
|
81
|
-
>
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
class LibsqlWebDriver implements Driver {
|
|
85
|
-
private readonly endpoint: string
|
|
86
|
-
private readonly authHeader: string | undefined
|
|
87
|
-
constructor(config: LibsqlWebDialectConfig) {
|
|
88
|
-
// Accept libsql://, https://, http://. The pipeline is HTTP only.
|
|
89
|
-
// Turso embeds the JWT in the URL as ?authToken=... — honor that, but let
|
|
90
|
-
// an explicit config.authToken win.
|
|
91
|
-
const u = new URL(config.url)
|
|
92
|
-
const scheme = u.protocol === 'libsql:' ? 'https:' : u.protocol
|
|
93
|
-
const tokenFromUrl = u.searchParams.get('authToken') ?? undefined
|
|
94
|
-
this.endpoint = `${scheme}//${u.host}/v2/pipeline`
|
|
95
|
-
const token = config.authToken ?? tokenFromUrl
|
|
96
|
-
this.authHeader = token ? `Bearer ${token}` : undefined
|
|
97
|
-
}
|
|
98
|
-
async init(): Promise<void> {}
|
|
99
|
-
async acquireConnection(): Promise<DatabaseConnection> {
|
|
100
|
-
return new LibsqlWebConnection(this.endpoint, this.authHeader)
|
|
101
|
-
}
|
|
102
|
-
async beginTransaction(conn: DatabaseConnection): Promise<void> {
|
|
103
|
-
await (conn as LibsqlWebConnection).executeRaw('BEGIN')
|
|
104
|
-
}
|
|
105
|
-
async commitTransaction(conn: DatabaseConnection): Promise<void> {
|
|
106
|
-
await (conn as LibsqlWebConnection).executeRaw('COMMIT')
|
|
107
|
-
}
|
|
108
|
-
async rollbackTransaction(conn: DatabaseConnection): Promise<void> {
|
|
109
|
-
await (conn as LibsqlWebConnection).executeRaw('ROLLBACK')
|
|
110
|
-
}
|
|
111
|
-
async releaseConnection(conn: DatabaseConnection): Promise<void> {
|
|
112
|
-
await (conn as LibsqlWebConnection).close()
|
|
113
|
-
}
|
|
114
|
-
async destroy(): Promise<void> {}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
class LibsqlWebConnection implements DatabaseConnection {
|
|
118
|
-
private baton: string | null = null
|
|
119
|
-
private endpoint: string
|
|
120
|
-
constructor(
|
|
121
|
-
initialEndpoint: string,
|
|
122
|
-
private readonly authHeader: string | undefined
|
|
123
|
-
) {
|
|
124
|
-
this.endpoint = initialEndpoint
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async executeQuery<R>(query: CompiledQuery): Promise<QueryResult<R>> {
|
|
128
|
-
const result = await this.runStmt(query.sql, query.parameters)
|
|
129
|
-
return decodeResult<R>(result)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async *streamQuery<R>(
|
|
133
|
-
query: CompiledQuery
|
|
134
|
-
): AsyncIterableIterator<QueryResult<R>> {
|
|
135
|
-
yield this.executeQuery<R>(query)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async executeRaw(sql: string): Promise<void> {
|
|
139
|
-
await this.runStmt(sql, [])
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async close(): Promise<void> {
|
|
143
|
-
if (this.baton === null) return
|
|
144
|
-
await this.send([{ type: 'close' }])
|
|
145
|
-
this.baton = null
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
private async runStmt(
|
|
149
|
-
sql: string,
|
|
150
|
-
args: readonly unknown[]
|
|
151
|
-
): Promise<HranaStmtResult> {
|
|
152
|
-
const resp = await this.send([
|
|
153
|
-
{
|
|
154
|
-
type: 'execute',
|
|
155
|
-
stmt: { sql, args: args.map(encodeArg) },
|
|
156
|
-
},
|
|
157
|
-
])
|
|
158
|
-
const r = resp.results[0]
|
|
159
|
-
if (!r) throw new Error('libsql: empty pipeline response')
|
|
160
|
-
if (r.type === 'error')
|
|
161
|
-
throw new Error(
|
|
162
|
-
`libsql: ${r.error.message}${r.error.code ? ` (${r.error.code})` : ''}`
|
|
163
|
-
)
|
|
164
|
-
if (r.response.type !== 'execute')
|
|
165
|
-
throw new Error(`libsql: unexpected response type ${r.response.type}`)
|
|
166
|
-
return r.response.result
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
private async send(requests: unknown[]): Promise<PipelineResponse> {
|
|
170
|
-
const headers: Record<string, string> = {
|
|
171
|
-
'content-type': 'application/json',
|
|
172
|
-
}
|
|
173
|
-
if (this.authHeader) headers.authorization = this.authHeader
|
|
174
|
-
const res = await fetch(this.endpoint, {
|
|
175
|
-
method: 'POST',
|
|
176
|
-
headers,
|
|
177
|
-
body: JSON.stringify({ baton: this.baton, requests }),
|
|
178
|
-
})
|
|
179
|
-
if (!res.ok) {
|
|
180
|
-
const body = await res.text().catch(() => '')
|
|
181
|
-
throw new Error(`libsql HTTP ${res.status}: ${body}`)
|
|
182
|
-
}
|
|
183
|
-
const json = (await res.json()) as PipelineResponse
|
|
184
|
-
this.baton = json.baton
|
|
185
|
-
if (json.base_url)
|
|
186
|
-
this.endpoint = `${json.base_url.replace(/\/$/, '')}/v2/pipeline`
|
|
187
|
-
return json
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Whether a value is a JSON column's worth of data rather than some other
|
|
193
|
-
* object that merely happens to be one.
|
|
194
|
-
*
|
|
195
|
-
* A `Map`, a `RegExp` or a class instance all stringify to `"{}"` or to a
|
|
196
|
-
* partial view of themselves, so accepting every object would silently persist
|
|
197
|
-
* an empty JSON blob where the caller meant something. Those fall through to
|
|
198
|
-
* the unsupported-type error instead. Kept in lockstep with the `coerce()` in
|
|
199
|
-
* the CLI's node:sqlite dev runtime (`db/sqlite/sqlite-kysely.ts`).
|
|
200
|
-
*/
|
|
201
|
-
function isJsonEncodable(v: object): boolean {
|
|
202
|
-
if (Array.isArray(v)) return true
|
|
203
|
-
const proto = Object.getPrototypeOf(v)
|
|
204
|
-
return proto === Object.prototype || proto === null
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Encodes one Kysely bind parameter as a Hrana value.
|
|
209
|
-
*
|
|
210
|
-
* The Date and plain-object branches exist for parity with the `coerce()` in
|
|
211
|
-
* the CLI's node:sqlite dev runtime (`db/sqlite/sqlite-kysely.ts`). Without
|
|
212
|
-
* them a query that binds a timestamp or a JSON column passes under
|
|
213
|
-
* `pikku dev` and throws `unsupported argument type object` once deployed to
|
|
214
|
-
* a Worker — so every write in a deployed libsql app fails while the same
|
|
215
|
-
* code works locally.
|
|
216
|
-
*/
|
|
217
|
-
function encodeArg(v: unknown): HranaValue {
|
|
218
|
-
if (v === null || v === undefined) return { type: 'null' }
|
|
219
|
-
if (typeof v === 'string') return { type: 'text', value: v }
|
|
220
|
-
if (typeof v === 'number')
|
|
221
|
-
return Number.isInteger(v)
|
|
222
|
-
? { type: 'integer', value: String(v) }
|
|
223
|
-
: { type: 'float', value: v }
|
|
224
|
-
if (typeof v === 'bigint') return { type: 'integer', value: v.toString() }
|
|
225
|
-
if (typeof v === 'boolean') return { type: 'integer', value: v ? '1' : '0' }
|
|
226
|
-
if (v instanceof ArrayBuffer)
|
|
227
|
-
return { type: 'blob', base64: bytesToBase64(new Uint8Array(v)) }
|
|
228
|
-
if (v instanceof Uint8Array) return { type: 'blob', base64: bytesToBase64(v) }
|
|
229
|
-
if (v instanceof Date) return { type: 'text', value: v.toISOString() }
|
|
230
|
-
if (typeof v === 'object' && isJsonEncodable(v))
|
|
231
|
-
return { type: 'text', value: JSON.stringify(v) }
|
|
232
|
-
// Name the constructor for objects: "unsupported argument type object" is
|
|
233
|
-
// what made the missing Date branch so hard to place in the first place.
|
|
234
|
-
const kind =
|
|
235
|
-
typeof v === 'object' ? (v.constructor?.name ?? 'object') : typeof v
|
|
236
|
-
throw new Error(`libsql: unsupported argument type ${kind}`)
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function decodeValue(v: HranaValue): unknown {
|
|
240
|
-
switch (v.type) {
|
|
241
|
-
case 'null':
|
|
242
|
-
return null
|
|
243
|
-
case 'integer': {
|
|
244
|
-
const n = Number(v.value)
|
|
245
|
-
return Number.isSafeInteger(n) ? n : BigInt(v.value)
|
|
246
|
-
}
|
|
247
|
-
case 'float':
|
|
248
|
-
return v.value
|
|
249
|
-
case 'text':
|
|
250
|
-
return v.value
|
|
251
|
-
case 'blob':
|
|
252
|
-
return base64ToBytes(v.base64)
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function decodeResult<R>(r: HranaStmtResult): QueryResult<R> {
|
|
257
|
-
const colNames = r.cols.map((c, i) => c.name ?? `c${i}`)
|
|
258
|
-
const rows = r.rows.map((row) => {
|
|
259
|
-
const o: Record<string, unknown> = {}
|
|
260
|
-
for (let i = 0; i < colNames.length; i++)
|
|
261
|
-
o[colNames[i]!] = decodeValue(row[i]!)
|
|
262
|
-
return o as R
|
|
263
|
-
})
|
|
264
|
-
return {
|
|
265
|
-
rows,
|
|
266
|
-
numAffectedRows: BigInt(r.affected_row_count),
|
|
267
|
-
insertId:
|
|
268
|
-
r.last_insert_rowid !== null ? BigInt(r.last_insert_rowid) : undefined,
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function bytesToBase64(b: Uint8Array): string {
|
|
273
|
-
let s = ''
|
|
274
|
-
for (let i = 0; i < b.length; i++) s += String.fromCharCode(b[i]!)
|
|
275
|
-
return btoa(s)
|
|
276
|
-
}
|
|
277
|
-
function base64ToBytes(s: string): Uint8Array {
|
|
278
|
-
const bin = atob(s)
|
|
279
|
-
const out = new Uint8Array(bin.length)
|
|
280
|
-
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)
|
|
281
|
-
return out
|
|
282
|
-
}
|
package/src/sqlite-functions.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* User-defined SQL functions, shared between the runtime-specific SQLite
|
|
3
|
-
* drivers so that `functions: { … }` means the same thing whichever one an app
|
|
4
|
-
* is wired to.
|
|
5
|
-
*
|
|
6
|
-
* Only `@pikku/kysely-node-sqlite` can actually register them. bun:sqlite has
|
|
7
|
-
* no equivalent of `db.function()`, so `@pikku/kysely-bun-sqlite` rejects this
|
|
8
|
-
* option outright rather than accepting it and letting the queries fail. That
|
|
9
|
-
* asymmetry is the whole reason this type is declared once, in a package both
|
|
10
|
-
* depend on: an app that names its UDFs against this type finds out at wiring
|
|
11
|
-
* time — or at the type level — that it cannot move to bun, instead of finding
|
|
12
|
-
* out as `no such function: …` from whichever endpoint happens to use it.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* A scalar SQL function. Arguments arrive as the values SQLite stores — string,
|
|
17
|
-
* number, bigint, Uint8Array or null — and the return value must be one SQLite
|
|
18
|
-
* can store, so no Date, boolean or object.
|
|
19
|
-
*/
|
|
20
|
-
export type SqliteFunction = (
|
|
21
|
-
...args: Array<string | number | bigint | Uint8Array | null>
|
|
22
|
-
) => string | number | bigint | Uint8Array | null
|
|
23
|
-
|
|
24
|
-
/** Scalar SQL functions to register, keyed by the name SQL will call them by. */
|
|
25
|
-
export type SqliteFunctionMap = Record<string, SqliteFunction>
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Thrown when UDFs are requested on a runtime that cannot provide them.
|
|
29
|
-
*
|
|
30
|
-
* Its own class rather than a plain Error because the one sensible reaction —
|
|
31
|
-
* fall back to doing the work outside SQL — is worth being able to catch for.
|
|
32
|
-
*/
|
|
33
|
-
export class SqliteFunctionsUnsupportedError extends Error {
|
|
34
|
-
constructor(
|
|
35
|
-
readonly runtime: string,
|
|
36
|
-
readonly functionNames: string[]
|
|
37
|
-
) {
|
|
38
|
-
super(
|
|
39
|
-
`${runtime} cannot register user-defined SQL functions, but ${functionNames.length} were requested: ${functionNames.join(', ')}.\n\n` +
|
|
40
|
-
`Every query calling them would fail with "no such function". Options:\n` +
|
|
41
|
-
` - run on Node with @pikku/kysely-node-sqlite, which supports them; or\n` +
|
|
42
|
-
` - do the work outside SQL, narrowing rows with an index first so the\n` +
|
|
43
|
-
` computation runs over a candidate set rather than the whole table.`
|
|
44
|
-
)
|
|
45
|
-
this.name = 'SqliteFunctionsUnsupportedError'
|
|
46
|
-
}
|
|
47
|
-
}
|
package/tsconfig.json
DELETED