@pikku/kysely-sqlite 0.12.8 → 0.12.9

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 CHANGED
@@ -1,5 +1,23 @@
1
1
  # @pikku/kysely-sqlite
2
2
 
3
+ ## 0.12.9
4
+
5
+ ### Patch Changes
6
+
7
+ - c335f73: `LibsqlWebDialect` now encodes a `Date` bind parameter as an ISO-8601 string and an array or plain record as JSON, instead of throwing `libsql: unsupported argument type object`.
8
+
9
+ This closes a dev/prod split that broke every write in a deployed libsql app. The CLI's `node:sqlite` dev runtime already coerced dates and objects before binding them, so a query that stamps a `createdAt` or writes a JSON column passed under `pikku dev` — and then threw on the same code path once the app was deployed to a Worker, where this dialect is the one in use. Reads were unaffected, so the symptom was an app whose every insert and update failed while every list and get worked.
10
+
11
+ Both runtimes also stop accepting objects JSON cannot faithfully represent. A `Map`, a `RegExp` or a class instance stringifies to `"{}"` or to a partial view of itself, so binding one used to persist an empty JSON blob where the caller meant something; it now throws. Only arrays and plain records (including null-prototype ones) are JSON-encoded. The two coercions are deliberately identical — a value that binds under `pikku dev` binds the same way once deployed, which is the property whose absence caused the original bug.
12
+
13
+ Unsupported values now name their constructor — `unsupported argument type Map` rather than `unsupported argument type object`.
14
+
15
+ - Updated dependencies [8a2c993]
16
+ - Updated dependencies [a261006]
17
+ - Updated dependencies [09973b9]
18
+ - @pikku/core@0.12.71
19
+ - @pikku/kysely@0.13.6
20
+
3
21
  ## 0.12.8
4
22
 
5
23
  ### Patch Changes
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 - present Yasser Fadl and Pikku contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # @pikku/kysely-sqlite
2
+
3
+ SQLite dialect services for Pikku — workflow state, secrets, channel and
4
+ event-hub stores — plus `LibsqlWebDialect` for libSQL/Turso over HTTP.
5
+
6
+ Takes a `SqliteDatabase` rather than opening one, so it works with any SQLite
7
+ driver. For the runtime built-ins use `@pikku/kysely-node-sqlite` or
8
+ `@pikku/kysely-bun-sqlite`.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install @pikku/kysely-sqlite
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```typescript
19
+ import {
20
+ createSQLiteKysely,
21
+ SQLiteKyselyWorkflowService,
22
+ } from '@pikku/kysely-sqlite'
23
+ import Database from 'better-sqlite3'
24
+
25
+ const db = createSQLiteKysely(new Database('app.db'))
26
+
27
+ const workflowService = new SQLiteKyselyWorkflowService(db)
28
+ ```
29
+
30
+ ## Docs
31
+
32
+ https://pikku.dev/docs
@@ -111,6 +111,32 @@ class LibsqlWebConnection {
111
111
  return json;
112
112
  }
113
113
  }
114
+ /**
115
+ * Whether a value is a JSON column's worth of data rather than some other
116
+ * object that merely happens to be one.
117
+ *
118
+ * A `Map`, a `RegExp` or a class instance all stringify to `"{}"` or to a
119
+ * partial view of themselves, so accepting every object would silently persist
120
+ * an empty JSON blob where the caller meant something. Those fall through to
121
+ * the unsupported-type error instead. Kept in lockstep with the `coerce()` in
122
+ * the CLI's node:sqlite dev runtime (`db/sqlite/sqlite-kysely.ts`).
123
+ */
124
+ function isJsonEncodable(v) {
125
+ if (Array.isArray(v))
126
+ return true;
127
+ const proto = Object.getPrototypeOf(v);
128
+ return proto === Object.prototype || proto === null;
129
+ }
130
+ /**
131
+ * Encodes one Kysely bind parameter as a Hrana value.
132
+ *
133
+ * The Date and plain-object branches exist for parity with the `coerce()` in
134
+ * the CLI's node:sqlite dev runtime (`db/sqlite/sqlite-kysely.ts`). Without
135
+ * them a query that binds a timestamp or a JSON column passes under
136
+ * `pikku dev` and throws `unsupported argument type object` once deployed to
137
+ * a Worker — so every write in a deployed libsql app fails while the same
138
+ * code works locally.
139
+ */
114
140
  function encodeArg(v) {
115
141
  if (v === null || v === undefined)
116
142
  return { type: 'null' };
@@ -128,7 +154,14 @@ function encodeArg(v) {
128
154
  return { type: 'blob', base64: bytesToBase64(new Uint8Array(v)) };
129
155
  if (v instanceof Uint8Array)
130
156
  return { type: 'blob', base64: bytesToBase64(v) };
131
- throw new Error(`libsql: unsupported argument type ${typeof v}`);
157
+ if (v instanceof Date)
158
+ return { type: 'text', value: v.toISOString() };
159
+ if (typeof v === 'object' && isJsonEncodable(v))
160
+ return { type: 'text', value: JSON.stringify(v) };
161
+ // Name the constructor for objects: "unsupported argument type object" is
162
+ // what made the missing Date branch so hard to place in the first place.
163
+ const kind = typeof v === 'object' ? (v.constructor?.name ?? 'object') : typeof v;
164
+ throw new Error(`libsql: unsupported argument type ${kind}`);
132
165
  }
133
166
  function decodeValue(v) {
134
167
  switch (v.type) {
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@pikku/kysely-sqlite",
3
- "version": "0.12.8",
3
+ "version": "0.12.9",
4
+ "description": "SQLite dialect services and libSQL web dialect for Pikku, backed by Kysely",
4
5
  "author": "yasser.fadl@gmail.com",
5
6
  "license": "MIT",
6
7
  "module": "dist/src/index.js",
@@ -10,17 +11,20 @@
10
11
  "tsc": "tsc",
11
12
  "ncu": "npx npm-check-updates",
12
13
  "build": "tsc -b",
14
+ "test": "bash run-tests.sh",
15
+ "test:watch": "bash run-tests.sh --watch",
16
+ "test:coverage": "bash run-tests.sh --coverage",
13
17
  "prepublishOnly": "yarn build"
14
18
  },
15
19
  "dependencies": {
16
- "@pikku/kysely": "^0.13.0",
20
+ "@pikku/kysely": "^0.13.6",
17
21
  "kysely": "^0.29.0"
18
22
  },
19
23
  "peerDependencies": {
20
- "@pikku/core": "^0.12.54"
24
+ "@pikku/core": "^0.12.71"
21
25
  },
22
26
  "devDependencies": {
23
- "@pikku/core": "^0.12.54",
27
+ "@pikku/core": "^0.12.71",
24
28
  "typescript": "^6.0.3"
25
29
  },
26
30
  "exports": {
package/run-tests.sh ADDED
@@ -0,0 +1,33 @@
1
+ #!/bin/bash
2
+
3
+ # Parse command line arguments
4
+ WATCH_MODE=false
5
+ COVERAGE_MODE=false
6
+
7
+ for arg in "$@"
8
+ do
9
+ case $arg in
10
+ --watch)
11
+ WATCH_MODE=true
12
+ shift
13
+ ;;
14
+ --coverage)
15
+ COVERAGE_MODE=true
16
+ shift
17
+ ;;
18
+ esac
19
+ done
20
+
21
+ # Build the command
22
+ CMD="node --import tsx --test --test-force-exit src/**/*.test.ts"
23
+
24
+ if [ "$WATCH_MODE" = true ]; then
25
+ CMD="$CMD --watch"
26
+ fi
27
+
28
+ if [ "$COVERAGE_MODE" = true ]; then
29
+ CMD="$CMD --experimental-test-coverage"
30
+ fi
31
+
32
+ # Execute the command
33
+ eval $CMD
@@ -0,0 +1,151 @@
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
+ })
@@ -189,6 +189,32 @@ class LibsqlWebConnection implements DatabaseConnection {
189
189
  }
190
190
  }
191
191
 
192
+ /**
193
+ * Whether a value is a JSON column's worth of data rather than some other
194
+ * object that merely happens to be one.
195
+ *
196
+ * A `Map`, a `RegExp` or a class instance all stringify to `"{}"` or to a
197
+ * partial view of themselves, so accepting every object would silently persist
198
+ * an empty JSON blob where the caller meant something. Those fall through to
199
+ * the unsupported-type error instead. Kept in lockstep with the `coerce()` in
200
+ * the CLI's node:sqlite dev runtime (`db/sqlite/sqlite-kysely.ts`).
201
+ */
202
+ function isJsonEncodable(v: object): boolean {
203
+ if (Array.isArray(v)) return true
204
+ const proto = Object.getPrototypeOf(v)
205
+ return proto === Object.prototype || proto === null
206
+ }
207
+
208
+ /**
209
+ * Encodes one Kysely bind parameter as a Hrana value.
210
+ *
211
+ * The Date and plain-object branches exist for parity with the `coerce()` in
212
+ * the CLI's node:sqlite dev runtime (`db/sqlite/sqlite-kysely.ts`). Without
213
+ * them a query that binds a timestamp or a JSON column passes under
214
+ * `pikku dev` and throws `unsupported argument type object` once deployed to
215
+ * a Worker — so every write in a deployed libsql app fails while the same
216
+ * code works locally.
217
+ */
192
218
  function encodeArg(v: unknown): HranaValue {
193
219
  if (v === null || v === undefined) return { type: 'null' }
194
220
  if (typeof v === 'string') return { type: 'text', value: v }
@@ -201,7 +227,14 @@ function encodeArg(v: unknown): HranaValue {
201
227
  if (v instanceof ArrayBuffer)
202
228
  return { type: 'blob', base64: bytesToBase64(new Uint8Array(v)) }
203
229
  if (v instanceof Uint8Array) return { type: 'blob', base64: bytesToBase64(v) }
204
- throw new Error(`libsql: unsupported argument type ${typeof v}`)
230
+ if (v instanceof Date) return { type: 'text', value: v.toISOString() }
231
+ if (typeof v === 'object' && isJsonEncodable(v))
232
+ return { type: 'text', value: JSON.stringify(v) }
233
+ // Name the constructor for objects: "unsupported argument type object" is
234
+ // what made the missing Date branch so hard to place in the first place.
235
+ const kind =
236
+ typeof v === 'object' ? (v.constructor?.name ?? 'object') : typeof v
237
+ throw new Error(`libsql: unsupported argument type ${kind}`)
205
238
  }
206
239
 
207
240
  function decodeValue(v: HranaValue): unknown {