zeed 1.5.0 → 1.8.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/package.json CHANGED
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "name": "zeed",
3
3
  "type": "module",
4
- "version": "1.5.0",
5
- "packageManager": "pnpm@10.33.0",
4
+ "version": "1.8.0",
6
5
  "description": "🌱 Simple foundation library",
7
6
  "author": {
8
7
  "name": "Dirk Holtwick",
@@ -56,6 +55,22 @@
56
55
  "engines": {
57
56
  "node": ">=20"
58
57
  },
58
+ "devDependencies": {
59
+ "@antfu/eslint-config": "^8.2.0",
60
+ "@antfu/ni": "^30.0.0",
61
+ "@types/node": "^25.6.0",
62
+ "@vitejs/plugin-vue": "^6.0.6",
63
+ "@vitest/browser": "^4.1.4",
64
+ "@vitest/coverage-v8": "^4.1.4",
65
+ "esbuild": "^0.28.0",
66
+ "eslint": "^10.2.1",
67
+ "playwright": "^1.59.1",
68
+ "pnpm": "^10.32.1",
69
+ "tsdown": "^0.21.9",
70
+ "typescript": "^6.0.3",
71
+ "vite": "^8.0.8",
72
+ "vitest": "^4.1.4"
73
+ },
59
74
  "scripts": {
60
75
  "build": "nr clean && NODE_ENV=production tsdown",
61
76
  "build:docs": "nlx typedoc --skipErrorChecking ./src/index.all.ts",
@@ -66,33 +81,15 @@
66
81
  "clean": "rm -rf dist",
67
82
  "coverage": "vitest --run --coverage",
68
83
  "lint": "eslint . --fix",
69
- "prepublishOnly": "nr build && nr circles",
70
84
  "start": "nr watch",
71
85
  "test": "nr check && vitest --run",
72
86
  "test:browser": "PREVIEW=1 vitest",
73
87
  "test:firefox": "BROWSER=firefox vitest",
74
88
  "test:webkit": "BROWSER=webkit vitest",
75
89
  "test:chromium": "BROWSER=chromium vitest",
76
- "test:release": "nr lint && nr check && vitest --run",
77
- "post:release": "nr upload:docs && nr npm:release",
78
- "npm:release": "npm login && npm publish --access public",
79
- "watch": "nr build -- --watch src",
80
- "prep": "nr lint && nr check && nr test:release && nr upload:docs"
81
- },
82
- "devDependencies": {
83
- "@antfu/eslint-config": "^8.2.0",
84
- "@antfu/ni": "^30.0.0",
85
- "@types/node": "^25.6.0",
86
- "@vitejs/plugin-vue": "^6.0.6",
87
- "@vitest/browser": "^4.1.4",
88
- "@vitest/coverage-v8": "^4.1.4",
89
- "esbuild": "^0.28.0",
90
- "eslint": "^10.2.1",
91
- "playwright": "^1.59.1",
92
- "pnpm": "^10.32.1",
93
- "tsdown": "^0.21.9",
94
- "typescript": "^6.0.3",
95
- "vite": "^8.0.8",
96
- "vitest": "^4.1.4"
90
+ "pre:release": "nr lint && nr check",
91
+ "test:release": "vitest --run",
92
+ "post:release": "nr upload:docs && pnpm publish",
93
+ "watch": "nr build -- --watch src"
97
94
  }
98
- }
95
+ }
@@ -1,4 +1,5 @@
1
- import { createDecoder, peekUint8, peekUint16, peekUint32, peekVarInt, peekVarString, peekVarUint, readAny, readBigInt64, readBigUint64, readFloat32, readFloat64, readUint8, readUint16, readUint32, readUint32BigEndian, readVarInt, readVarString, readVarUint, readVarUint8Array } from './decoding'
1
+ import { afterEach, vi } from 'vitest'
2
+ import { clone, createDecoder, hasContent, peekUint8, peekUint16, peekUint32, peekVarInt, peekVarString, peekVarUint, readAny, readBigInt64, readBigUint64, readFloat32, readFloat64, readTailAsUint8Array, readUint8, readUint16, readUint32, readUint32BigEndian, readVarInt, readVarString, readVarUint, readVarUint8Array, skip8 } from './decoding'
2
3
  import { createBinEncoder, writeAny, writeBigInt64, writeBigUint64, writeFloat32, writeFloat64, writeUint8, writeUint16, writeUint32, writeVarInt, writeVarString, writeVarUint, writeVarUint8Array } from './encoding'
3
4
 
4
5
  describe('lib0/decoding', () => {
@@ -79,6 +80,88 @@ describe('lib0/decoding', () => {
79
80
  expect(readAny(d)).toEqual({ a: 1, b: 2 })
80
81
  })
81
82
 
83
+ it('hasContent reflects position', () => {
84
+ const d = createDecoder(new Uint8Array([1, 2]))
85
+ expect(hasContent(d)).toBe(true)
86
+ readUint8(d)
87
+ readUint8(d)
88
+ expect(hasContent(d)).toBe(false)
89
+ })
90
+
91
+ it('clone preserves and overrides position', () => {
92
+ const d = createDecoder(new Uint8Array([1, 2, 3]))
93
+ readUint8(d)
94
+ const c1 = clone(d)
95
+ expect(c1.pos).toBe(1)
96
+ expect(c1.arr).toBe(d.arr)
97
+ const c2 = clone(d, 0)
98
+ expect(c2.pos).toBe(0)
99
+ })
100
+
101
+ it('skip8 advances one byte', () => {
102
+ const d = createDecoder(new Uint8Array([7, 8]))
103
+ skip8(d)
104
+ expect(d.pos).toBe(1)
105
+ expect(readUint8(d)).toBe(8)
106
+ })
107
+
108
+ it('readTailAsUint8Array returns remaining bytes', () => {
109
+ const d = createDecoder(new Uint8Array([1, 2, 3, 4]))
110
+ readUint8(d)
111
+ const tail = readTailAsUint8Array(d)
112
+ expect([...tail]).toEqual([2, 3, 4])
113
+ })
114
+
115
+ it('readVarUint throws on unexpected end of array', () => {
116
+ const d = createDecoder(new Uint8Array([0x80, 0x80]))
117
+ expect(() => readVarUint(d)).toThrow(/Unexpected end of array/)
118
+ })
119
+
120
+ it('readVarInt throws on unexpected end of array', () => {
121
+ const d = createDecoder(new Uint8Array([0x80]))
122
+ expect(() => readVarInt(d)).toThrow(/Unexpected end of array/)
123
+ })
124
+
125
+ it('readVarUint throws on integer out of range', () => {
126
+ const bytes = new Uint8Array(12).fill(0xFF)
127
+ bytes[11] = 0x7F
128
+ const d = createDecoder(bytes)
129
+ expect(() => readVarUint(d)).toThrow(/Integer out of Range/)
130
+ })
131
+
132
+ it('readVarInt throws on integer out of range', () => {
133
+ const bytes = new Uint8Array(12).fill(0xFF)
134
+ bytes[11] = 0x7F
135
+ const d = createDecoder(bytes)
136
+ expect(() => readVarInt(d)).toThrow(/Integer out of Range/)
137
+ })
138
+
139
+ describe('polyfill path', () => {
140
+ afterEach(() => {
141
+ vi.unstubAllGlobals()
142
+ vi.resetModules()
143
+ })
144
+
145
+ it('readVarString via polyfill (empty, small, large)', async () => {
146
+ vi.stubGlobal('TextDecoder', undefined)
147
+ vi.resetModules()
148
+ const dec = await import('./decoding')
149
+ const enc = await import('./encoding')
150
+
151
+ const e = enc.createBinEncoder()
152
+ enc.writeVarString(e, '')
153
+ enc.writeVarString(e, 'abc')
154
+ const big = 'y'.repeat(15000)
155
+ enc.writeVarString(e, big)
156
+ const arr = enc.encodeToUint8Array(e)
157
+
158
+ const d = dec.createDecoder(arr)
159
+ expect(dec.readVarString(d)).toBe('')
160
+ expect(dec.readVarString(d)).toBe('abc')
161
+ expect(dec.readVarString(d)).toBe(big)
162
+ })
163
+ })
164
+
82
165
  it('should peek values', () => {
83
166
  const e = createBinEncoder()
84
167
  writeUint8(e, 42)
@@ -1,4 +1,6 @@
1
- import { createBinEncoder, encodeToUint8Array, isNegativeZero, setUint8, setUint16, setUint32, writeAny, writeBigInt64, writeBigUint64, writeFloat32, writeFloat64, writeUint8, writeUint8Array, writeUint16, writeUint32, writeUint32BigEndian, writeVarInt, writeVarString, writeVarUint, writeVarUint8Array } from './encoding'
1
+ import { afterEach, vi } from 'vitest'
2
+ import { createDecoder, readAny, readVarInt as readVarIntDec, readVarString } from './decoding'
3
+ import { createBinEncoder, encodeToUint8Array, isNegativeZero, length, setUint8, setUint16, setUint32, verifyLen, writeAny, writeBigInt64, writeBigUint64, writeBinaryEncoder, writeFloat32, writeFloat64, writeUint8, writeUint8Array, writeUint16, writeUint32, writeUint32BigEndian, writeVarInt, writeVarString, writeVarUint, writeVarUint8Array } from './encoding'
2
4
 
3
5
  describe('lib0/encoding', () => {
4
6
  it('should encode/decode uint8/16/32', () => {
@@ -84,4 +86,110 @@ describe('lib0/encoding', () => {
84
86
  expect(isNegativeZero(1)).toBe(false)
85
87
  expect(isNegativeZero(-1)).toBe(true)
86
88
  })
89
+
90
+ it('grows buffers when exceeding initial capacity', () => {
91
+ const e = createBinEncoder()
92
+ for (let i = 0; i < 250; i++) writeUint8(e, i & 0xFF)
93
+ expect(length(e)).toBe(250)
94
+ expect(e.bufs.length).toBeGreaterThan(0)
95
+ const arr = encodeToUint8Array(e)
96
+ expect(arr.length).toBe(250)
97
+ expect(arr[0]).toBe(0)
98
+ expect(arr[249]).toBe(249 & 0xFF)
99
+ })
100
+
101
+ it('verifyLen allocates a fresh buffer when required', () => {
102
+ const e = createBinEncoder()
103
+ writeUint8(e, 1)
104
+ verifyLen(e, 200)
105
+ expect(e.bufs.length).toBe(1)
106
+ writeUint8(e, 2)
107
+ const arr = encodeToUint8Array(e)
108
+ expect(arr[0]).toBe(1)
109
+ expect(arr[1]).toBe(2)
110
+ })
111
+
112
+ it('set updates bytes across multiple buffers', () => {
113
+ const e = createBinEncoder()
114
+ for (let i = 0; i < 250; i++) writeUint8(e, 0)
115
+ setUint8(e, 0, 0xAA)
116
+ setUint8(e, 120, 0xBB)
117
+ setUint8(e, 240, 0xCC)
118
+ const arr = encodeToUint8Array(e)
119
+ expect(arr[0]).toBe(0xAA)
120
+ expect(arr[120]).toBe(0xBB)
121
+ expect(arr[240]).toBe(0xCC)
122
+ })
123
+
124
+ it('writeUint8Array splits across buffers', () => {
125
+ const e = createBinEncoder()
126
+ writeUint8(e, 9)
127
+ const data = new Uint8Array(300)
128
+ for (let i = 0; i < data.length; i++) data[i] = i & 0xFF
129
+ writeUint8Array(e, data)
130
+ const arr = encodeToUint8Array(e)
131
+ expect(arr.length).toBe(301)
132
+ expect(arr[0]).toBe(9)
133
+ expect(arr[1]).toBe(0)
134
+ expect(arr[300]).toBe(299 & 0xFF)
135
+ })
136
+
137
+ it('writeBinaryEncoder appends another encoder', () => {
138
+ const inner = createBinEncoder()
139
+ writeUint8(inner, 1)
140
+ writeUint8(inner, 2)
141
+ const outer = createBinEncoder()
142
+ writeUint8(outer, 0)
143
+ writeBinaryEncoder(outer, inner)
144
+ const arr = encodeToUint8Array(outer)
145
+ expect([...arr]).toEqual([0, 1, 2])
146
+ })
147
+
148
+ it('writes long strings via alternate path', () => {
149
+ const e = createBinEncoder()
150
+ const big = 'z'.repeat(15000)
151
+ writeVarString(e, big)
152
+ const arr = encodeToUint8Array(e)
153
+ const d = createDecoder(arr)
154
+ expect(readVarString(d)).toBe(big)
155
+ })
156
+
157
+ it('writeVarInt handles multi-byte large values', () => {
158
+ const e = createBinEncoder()
159
+ writeVarInt(e, 1_000_000)
160
+ writeVarInt(e, -1_000_000)
161
+ const arr = encodeToUint8Array(e)
162
+ const d = createDecoder(arr)
163
+ expect(readVarIntDec(d)).toBe(1_000_000)
164
+ expect(readVarIntDec(d)).toBe(-1_000_000)
165
+ })
166
+
167
+ it('writeAny encodes float32-representable numbers compactly', () => {
168
+ const e = createBinEncoder()
169
+ writeAny(e, 0.5)
170
+ const arr = encodeToUint8Array(e)
171
+ expect(arr[0]).toBe(124)
172
+ const d = createDecoder(arr)
173
+ expect(readAny(d)).toBe(0.5)
174
+ })
175
+
176
+ describe('polyfill path', () => {
177
+ afterEach(() => {
178
+ vi.unstubAllGlobals()
179
+ vi.resetModules()
180
+ })
181
+
182
+ it('writeVarString via polyfill when TextEncoder missing', async () => {
183
+ vi.stubGlobal('TextEncoder', undefined)
184
+ vi.stubGlobal('TextDecoder', undefined)
185
+ vi.resetModules()
186
+ const enc = await import('./encoding')
187
+ const dec = await import('./decoding')
188
+ const e = enc.createBinEncoder()
189
+ enc.writeVarString(e, 'hi✓')
190
+ const arr = enc.encodeToUint8Array(e)
191
+ const d = dec.createDecoder(arr)
192
+ expect(dec.readVarString(d)).toBe('hi✓')
193
+ })
194
+ })
87
195
  })
@@ -1,4 +1,5 @@
1
- import { decodeUtf8, encodeUtf8, fromCamelCase, splice, trimLeft, utf8ByteLength } from './string'
1
+ import { afterEach, vi } from 'vitest'
2
+ import { _encodeUtf8Polyfill, decodeUtf8, encodeUtf8, fromCamelCase, splice, trimLeft, utf8ByteLength } from './string'
2
3
 
3
4
  describe('lib0/string', () => {
4
5
  it('should trim left', () => {
@@ -31,4 +32,47 @@ describe('lib0/string', () => {
31
32
  expect(splice('abcdef', 0, 3, 'Z')).toBe('Zdef')
32
33
  expect(splice('abcdef', 3, 0, 'Q')).toBe('abcQdef')
33
34
  })
35
+
36
+ it('should encode utf8 via polyfill', () => {
37
+ const bytes = _encodeUtf8Polyfill('hi✓')
38
+ expect(bytes).toBeInstanceOf(Uint8Array)
39
+ expect(decodeUtf8(bytes)).toBe('hi✓')
40
+ })
41
+
42
+ describe('polyfill paths via module reset', () => {
43
+ afterEach(() => {
44
+ vi.unstubAllGlobals()
45
+ vi.resetModules()
46
+ })
47
+
48
+ it('uses polyfill when TextEncoder/TextDecoder unavailable', async () => {
49
+ vi.stubGlobal('TextEncoder', undefined)
50
+ vi.stubGlobal('TextDecoder', undefined)
51
+ vi.resetModules()
52
+ const mod = await import('./string')
53
+ const encoded = mod.encodeUtf8('hello✓')
54
+ expect(encoded).toBeInstanceOf(Uint8Array)
55
+ expect(mod.decodeUtf8(encoded)).toBe('hello✓')
56
+ expect(mod.getUtf8TextEncoder()).toBeNull()
57
+ })
58
+
59
+ it('decodes via polyfill for a large buffer', async () => {
60
+ vi.stubGlobal('TextDecoder', undefined)
61
+ vi.resetModules()
62
+ const mod = await import('./string')
63
+ const big = 'a'.repeat(15000)
64
+ const bytes = new TextEncoder().encode(big)
65
+ expect(mod.decodeUtf8(bytes)).toBe(big)
66
+ })
67
+
68
+ it('disables broken TextDecoder (Safari BOM workaround)', async () => {
69
+ class FakeDecoder {
70
+ decode() { return 'x' }
71
+ }
72
+ vi.stubGlobal('TextDecoder', FakeDecoder as any)
73
+ vi.resetModules()
74
+ const mod = await import('./string')
75
+ expect(mod.getUtf8TextDecoder()).toBeNull()
76
+ })
77
+ })
34
78
  })
@@ -84,6 +84,13 @@ describe('crypto', () => {
84
84
  expect(typeof key).toBe('object')
85
85
  })
86
86
 
87
+ it('should derive keys without salt (default branch)', async () => {
88
+ const key = await deriveKeyPbkdf2(new Uint8Array([1, 2, 3]))
89
+ expect(key).toBeDefined()
90
+ const keyCbc = await (await import('./crypto')).deriveKeyPbkdf2CBC(new Uint8Array([1, 2, 3]))
91
+ expect(keyCbc).toBeDefined()
92
+ })
93
+
87
94
  it('should handle zero-length random and digest', async () => {
88
95
  expect(randomUint8Array(0)).toEqual(new Uint8Array(0))
89
96
  expect((await digest(new Uint8Array(0))).length).toBeGreaterThan(0)
@@ -80,6 +80,54 @@ describe('xaes.spec', () => {
80
80
  expect(new Uint8Array(decrypted)).toEqual(plaintext)
81
81
  })
82
82
 
83
+ it('xaes rejects wrong algorithm key', async () => {
84
+ const key = await crypto.subtle.generateKey(
85
+ { name: 'AES-GCM', length: 256 },
86
+ false,
87
+ ['encrypt', 'decrypt'],
88
+ )
89
+ const iv = new Uint8Array(24)
90
+ await expect(encryptXAES({ iv }, key, new Uint8Array(1))).rejects.toThrow(/AES-CBC/)
91
+ })
92
+
93
+ it('xaes rejects key without encrypt usage', async () => {
94
+ const key = await crypto.subtle.importKey(
95
+ 'raw',
96
+ new Uint8Array(32).fill(0x05),
97
+ { name: 'AES-CBC', length: 256 },
98
+ false,
99
+ ['decrypt'],
100
+ )
101
+ const iv = new Uint8Array(24)
102
+ await expect(encryptXAES({ iv }, key, new Uint8Array(1))).rejects.toThrow(/encrypt/)
103
+ })
104
+
105
+ it('xaes rejects key of wrong length', async () => {
106
+ const key = await crypto.subtle.importKey(
107
+ 'raw',
108
+ new Uint8Array(16).fill(0x06),
109
+ { name: 'AES-CBC', length: 128 },
110
+ false,
111
+ ['encrypt', 'decrypt'],
112
+ )
113
+ const iv = new Uint8Array(24)
114
+ await expect(encryptXAES({ iv }, key, new Uint8Array(1))).rejects.toThrow(/256 bits/)
115
+ })
116
+
117
+ it('xaes rejects iv of wrong length', async () => {
118
+ const key = await generateKeyXAES()
119
+ await expect(encryptXAES({ iv: new Uint8Array(12) }, key, new Uint8Array(1))).rejects.toThrow(/24 bytes/)
120
+ })
121
+
122
+ it('xaes accepts iv as ArrayBuffer', async () => {
123
+ const key = await generateKeyXAES()
124
+ const ivBuffer = new Uint8Array(24).fill(7).buffer
125
+ const data = new Uint8Array(10).fill(2)
126
+ const encrypted = await encryptXAES({ iv: ivBuffer }, key, data)
127
+ const decrypted = await decryptXAES({ iv: ivBuffer }, key, encrypted)
128
+ expect(new Uint8Array(decrypted)).toEqual(data)
129
+ })
130
+
83
131
  // it('xaes test vector, accumulated', async () => {
84
132
  // const hash = new SHAKE128()
85
133
  // const rng = new SHAKE128()
@@ -174,6 +174,51 @@ describe('deep', () => {
174
174
  // // expect(a.one.sample.a === b.one.sample.a).toBe(true)
175
175
  // })
176
176
 
177
+ it('deepEqual handles branch cases', () => {
178
+ expect(deepEqual(1, 2)).toBe(false)
179
+ expect(deepEqual(null, {})).toBe(false)
180
+ expect(deepEqual({}, null)).toBe(false)
181
+ class A { x = 1 }
182
+ class B { x = 1 }
183
+ expect(deepEqual(new A(), new B())).toBe(false)
184
+ expect(deepEqual({ a: 1 }, { b: 1 })).toBe(false)
185
+ expect(deepEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false)
186
+ expect(deepEqual({ a: 1, b: 2 }, { a: 1, c: 2 })).toBe(false)
187
+ expect(deepEqual({ a: 1 }, { a: 1 })).toBe(true)
188
+
189
+ const proto = { inherited: 1 }
190
+ const x: any = Object.create(proto)
191
+ x.own = 1
192
+ const y: any = Object.create(proto)
193
+ y.own = 1
194
+ expect(deepEqual(x, y)).toBe(true)
195
+ })
196
+
197
+ it('deepStripUndefinedInPlace handles cycles and primitives', () => {
198
+ expect(deepStripUndefinedInPlace(5)).toBe(5)
199
+ const cyc: any = { a: 1 }
200
+ cyc.self = cyc
201
+ const result = deepStripUndefinedInPlace(cyc)
202
+ expect(result).toBe(cyc)
203
+ expect(cyc.a).toBe(1)
204
+ const obj: any = Object.create({ inherited: 1 })
205
+ obj.own = undefined
206
+ obj.keep = 2
207
+ deepStripUndefinedInPlace(obj)
208
+ expect('own' in obj).toBe(false)
209
+ expect(obj.keep).toBe(2)
210
+ })
211
+
212
+ it('deepMerge handles non-object target and source', () => {
213
+ expect(deepMerge(null as any, { a: 1 })).toEqual({ a: 1 })
214
+ expect(deepMerge({ a: 1 }, null)).toEqual({ a: 1 })
215
+ expect(deepMerge({ a: 1 }, 'str' as any)).toEqual({ a: 1 })
216
+ })
217
+
218
+ it('deepMerge concatenates arrays', () => {
219
+ expect(deepMerge({ list: [1, 2] }, { list: [3, 4] })).toEqual({ list: [1, 2, 3, 4] })
220
+ })
221
+
177
222
  it('should strip undefined', () => {
178
223
  const sample = {
179
224
  hello: {
@@ -0,0 +1,30 @@
1
+ import { escapeRegExp } from './regexp'
2
+
3
+ describe('escapeRegExp', () => {
4
+ it('returns empty string for falsy input', () => {
5
+ expect(escapeRegExp('')).toBe('')
6
+ // @ts-expect-error testing falsy
7
+ expect(escapeRegExp(undefined)).toBe('')
8
+ // @ts-expect-error testing falsy
9
+ expect(escapeRegExp(null)).toBe('')
10
+ })
11
+
12
+ it('returns source of RegExp input', () => {
13
+ expect(escapeRegExp(/foo\.bar/)).toBe('foo\\.bar')
14
+ })
15
+
16
+ it('escapes all special characters', () => {
17
+ expect(escapeRegExp('\\-[]/{}()*+?.^$|')).toBe('\\\\\\-\\[\\]\\/\\{\\}\\(\\)\\*\\+\\?\\.\\^\\$\\|')
18
+ })
19
+
20
+ it('leaves normal characters alone', () => {
21
+ expect(escapeRegExp('hello world 123')).toBe('hello world 123')
22
+ })
23
+
24
+ it('escaped string works as literal in RegExp', () => {
25
+ const src = 'a.b+c'
26
+ const re = new RegExp(escapeRegExp(src))
27
+ expect(re.test('a.b+c')).toBe(true)
28
+ expect(re.test('axbxc')).toBe(false)
29
+ })
30
+ })
@@ -7,7 +7,6 @@ export * from './parse-object'
7
7
  export * from './schema'
8
8
  export * from './schema-standard'
9
9
  export * from './serialize'
10
- export * from './sql'
11
10
  export * from './type-test'
12
11
  export * from './utils'
13
12
  export * from './z'
@@ -1,13 +0,0 @@
1
- // class TypeClass<T = unknown> {
2
- // optional(): TypeClass<T | undefined> {
3
- // return this
4
- // }
5
- // }
6
-
7
- // class TypeStringClass<T extends string> extends TypeClass<T> {
8
-
9
- // }
10
-
11
- // const o = new TypeStringClass()
12
- // const v = o.optional()
13
- // type t = typeof v // expect: TypeStringClass<string | undefined>
@@ -1,42 +0,0 @@
1
- // // Function
2
-
3
- // interface Type<T = unknown> {
4
- // _value?: T
5
- // }
6
-
7
- // type Infer<T> = T extends Type<infer TT> ? TT : never
8
-
9
- // type TupleOutput<T extends Type[]> = {
10
- // [K in keyof T]: T[K] extends Type<infer U> ? U : never;
11
- // }
12
-
13
- // type ArrayOutput<Head extends Type[], Rest extends Type | undefined> = [
14
- // ...TupleOutput<Head>,
15
- // ...(Rest extends Type ? Infer<Rest>[] : []),
16
- // ]
17
-
18
- // type ArrayType<
19
- // Head extends Type[] = Type[],
20
- // Rest extends Type | undefined = Type | undefined,
21
- // > = Type<ArrayOutput<Head, Rest>>
22
-
23
- // function tuple<T extends [] | [Type, ...Type[]]>(items: T): ArrayType<T, undefined> {
24
- // return {} as any
25
- // }
26
-
27
- // function string(): Type<string> {
28
- // return {} as any
29
- // }
30
-
31
- // function boolean(): Type<boolean> {
32
- // return {} as any
33
- // }
34
-
35
- // function number(): Type<number> {
36
- // return {} as any
37
- // }
38
-
39
- // const tt = tuple([number(), string(), boolean()])
40
- // type ttt = Infer<typeof tt> // expected [number, string, boolean]
41
-
42
- // //
@@ -1,45 +0,0 @@
1
- // /* eslint-disable ts/no-unsafe-declaration-merging */
2
- // // // Define an interface for the instance type
3
- // // interface MyTypeInstance {
4
- // // value: number
5
- // // test: () => number
6
- // // }
7
-
8
- // // // Constructor function
9
- // // function MyType(this: MyTypeInstance, value: number) {
10
- // // this.value = value
11
- // // }
12
-
13
- // // MyType.prototype.test = function () {
14
- // // return this.value
15
- // // }
16
-
17
- // class MyType {
18
- // value: number
19
- // constructor(value: number) {
20
- // this.value = value
21
- // }
22
- // }
23
-
24
- // interface MyType {
25
- // test: () => number
26
- // }
27
-
28
- // describe('sandbox.spec', () => {
29
- // it('should do something', async () => {
30
- // // Create an instance of MyType using the 'new' keyword
31
- // const my = new MyType(123)
32
-
33
- // MyType.prototype.test = function () {
34
- // return this.value + 1
35
- // }
36
-
37
- // // Example usage
38
- // expect(my.test()).toMatchInlineSnapshot(`124`)
39
- // expect(my).toMatchInlineSnapshot(`
40
- // MyType {
41
- // "value": 123,
42
- // }
43
- // `)
44
- // })
45
- // })
@@ -1,254 +0,0 @@
1
- # SQL Select Builder
2
-
3
- A minimal, type-safe SQL `SELECT` builder built on top of zeed's `z` schema system. Inspired by Drizzle ORM and Kysely, but deliberately small: single-table queries, no joins, no inserts/updates/deletes.
4
-
5
- The goal is to get full TypeScript inference, editor autocompletion, and - uniquely - per-query dependency tracking for reactive use cases.
6
-
7
- ## Features
8
-
9
- - Type-safe table definitions backed by `z` schemas
10
- - Full row type inference for results, including narrowed `pick`/`select` forms
11
- - Editor autocompletion on column keys
12
- - Parameterized SQL output (`?` placeholders, values returned separately)
13
- - Per-query dependency tracking split by role: `select`, `where`, `orderBy`
14
- - Zero runtime dependencies, tree-shakable
15
-
16
- ## Quick Start
17
-
18
- ```ts
19
- import { and, boolean, eq, from, gt, inArray, int, like, or, string, table } from 'zeed'
20
-
21
- const users = table('users', {
22
- id: int(),
23
- name: string(),
24
- email: string(),
25
- age: int(),
26
- active: boolean(),
27
- })
28
-
29
- const query = from(users)
30
- .pick('id', 'name')
31
- .where(and(eq(users.active, true), gt(users.age, 18)))
32
- .orderBy(users.name)
33
- .limit(20)
34
-
35
- const { sql, params } = query.toSQL()
36
- // sql: SELECT "users"."id", "users"."name" FROM "users"
37
- // WHERE ("users"."active" = ?) AND ("users"."age" > ?)
38
- // ORDER BY "users"."name" ASC LIMIT ?
39
- // params: [true, 18, 20]
40
- ```
41
-
42
- ## Defining Tables
43
-
44
- A table pairs a name with a `z`-schema shape. Each column becomes a typed reference usable in expressions.
45
-
46
- ```ts
47
- const posts = table('posts', {
48
- id: int(),
49
- title: string(),
50
- body: string(),
51
- authorId: int(),
52
- published: boolean(),
53
- })
54
-
55
- posts.id // Column<number>
56
- posts.title // Column<string>
57
- ```
58
-
59
- The shape is a plain `Record<string, Type>`, so any `z` type works (`string`, `int`, `boolean`, `stringLiterals`, ...).
60
-
61
- ## Selecting Columns
62
-
63
- Three forms, depending on how much control you need.
64
-
65
- ### 1. Full row (default)
66
-
67
- ```ts
68
- from(users)
69
- // Row: { id: number, name: string, email: string, age: number, active: boolean }
70
- ```
71
-
72
- ### 2. `pick(...keys)` - compact and typed
73
-
74
- ```ts
75
- from(users).pick('id', 'email')
76
- // Row: { id: number, email: string }
77
- ```
78
-
79
- Keys are typed against the table shape, so the editor autocompletes valid column names and rejects unknown ones at compile time.
80
-
81
- ### 3. `select({ alias: column })` - for aliases or mixed expressions
82
-
83
- ```ts
84
- import { select } from 'zeed'
85
-
86
- select({ uid: users.id, n: users.name }).from(users)
87
- // Row: { uid: number, n: string }
88
- // SQL: SELECT "users"."id" AS "uid", "users"."name" AS "n" FROM "users"
89
- ```
90
-
91
- ## Where Clauses
92
-
93
- Expressions are built from operator helpers. Columns and literal values can be mixed freely - literals are automatically bound as parameters.
94
-
95
- ```ts
96
- import { and, eq, gt, gte, inArray, like, lt, ne, or, sqlIsNotNull, sqlIsNull } from 'zeed'
97
-
98
- from(users).where(
99
- and(
100
- eq(users.active, true),
101
- or(
102
- like(users.name, 'A%'),
103
- inArray(users.id, [1, 2, 3]),
104
- ),
105
- sqlIsNotNull(users.email),
106
- ),
107
- )
108
- ```
109
-
110
- Available operators:
111
-
112
- | Helper | SQL |
113
- | --------------- | ------------ |
114
- | `eq(a, b)` | `a = b` |
115
- | `ne(a, b)` | `a <> b` |
116
- | `gt(a, b)` | `a > b` |
117
- | `gte(a, b)` | `a >= b` |
118
- | `lt(a, b)` | `a < b` |
119
- | `lte(a, b)` | `a <= b` |
120
- | `like(a, b)` | `a LIKE b` |
121
- | `inArray(a, [])`| `a IN (...)` |
122
- | `sqlIsNull(a)` | `a IS NULL` |
123
- | `sqlIsNotNull(a)` | `a IS NOT NULL` |
124
- | `and(...)` | `(..) AND (..)` |
125
- | `or(...)` | `(..) OR (..)` |
126
- | `not(e)` | `NOT (..)` |
127
-
128
- `and` and `or` accept `undefined`, `false`, and `null` entries and drop them, which is handy for conditional filters.
129
-
130
- Note: `sqlIsNull` and `sqlIsNotNull` are prefixed to avoid collision with zeed's existing `isNull`/`isNotNull` data helpers.
131
-
132
- ## Ordering, Limit, Offset
133
-
134
- ```ts
135
- from(users)
136
- .orderBy(users.age, 'DESC')
137
- .orderBy(users.name) // ASC by default
138
- .limit(10)
139
- .offset(20)
140
- ```
141
-
142
- ## Compiling a Query
143
-
144
- ```ts
145
- const { sql, params, dependencies } = query.toSQL()
146
- ```
147
-
148
- - `sql` - the query string with `?` placeholders
149
- - `params` - array of bound values in order
150
- - `dependencies` - see below
151
-
152
- Pass `sql` and `params` to whichever SQLite/Postgres/MySQL driver you use.
153
-
154
- ## Dependency Tracking
155
-
156
- Every compiled query reports exactly which table columns it touches, split by role. This is the key building block for reactive queries: when the database changes, you can decide whether a given query needs to re-run based on which columns were affected.
157
-
158
- ```ts
159
- const q = from(users)
160
- .pick('id', 'name')
161
- .where(eq(users.email, 'a@b.c'))
162
- .orderBy(users.age, 'DESC')
163
-
164
- q.toSQL().dependencies
165
- // [
166
- // {
167
- // table: 'users',
168
- // select: ['id', 'name'], // fields returned to the caller
169
- // where: ['email'], // fields used in filters
170
- // orderBy: ['age'], // fields used for ordering
171
- // all: ['age', 'email', 'id', 'name'], // union of the above
172
- // }
173
- // ]
174
- ```
175
-
176
- ### Example: reactive invalidation
177
-
178
- ```ts
179
- function isAffected(
180
- deps: readonly QueryDependencies[],
181
- change: { table: string, columns: string[] },
182
- ): boolean {
183
- return deps.some(d =>
184
- d.table === change.table
185
- && d.all.some(c => change.columns.includes(c)),
186
- )
187
- }
188
-
189
- // On a write notification from the database:
190
- if (isAffected(query.toSQL().dependencies, { table: 'users', columns: ['email'] })) {
191
- // re-run the query, push new rows to subscribers
192
- }
193
- ```
194
-
195
- Splitting by role lets you optimize further:
196
-
197
- - A change to a `select` column means the result *shape* changes - re-render.
198
- - A change to a `where` or `orderBy` column means the result *set* may change - re-query.
199
- - A change to a column not in `all` is irrelevant - skip.
200
-
201
- ## Type Inference
202
-
203
- Row types flow through the builder. Use `InferRow` to extract the result type for downstream code:
204
-
205
- ```ts
206
- import type { InferRow } from 'zeed'
207
-
208
- const query = from(users).pick('id', 'email')
209
- type Row = InferRow<typeof query> // { id: number, email: string }
210
-
211
- function render(rows: Row[]) { /* ... */ }
212
- render(await run(query))
213
- ```
214
-
215
- ## Scope and Limitations
216
-
217
- By design, this builder is minimal:
218
-
219
- - `SELECT` only. No `INSERT`, `UPDATE`, `DELETE`, `CREATE TABLE`.
220
- - Single table per query. No `JOIN`, no subqueries, no CTEs.
221
- - No aggregate functions (`COUNT`, `SUM`, `GROUP BY`, `HAVING`).
222
- - One SQL dialect (`?` placeholders, double-quoted identifiers). Works with SQLite and, with most drivers, Postgres.
223
-
224
- The existing `select({...}).from(...)` form remains available for cases that need column aliases. If you need joins or mutations, use a dedicated ORM - this module is intended for read-only queries in reactive contexts where dependency tracking is the main value-add.
225
-
226
- ## API Reference
227
-
228
- ### `table(name, shape)`
229
-
230
- Creates a table handle. `shape` is a `Record<string, Type>` from `z`. Returns an object where each key is a typed `Column` plus `_name` and `_shape` metadata.
231
-
232
- ### `from(table)`
233
-
234
- Starts a query. Returns a `SelectBuilder` with the full row type as its default result.
235
-
236
- ### `select(selection?)`
237
-
238
- Alternative entry point. With no argument, behaves like a full-row select (requires a subsequent `.from(...)`). With a `{ alias: column }` map, creates an aliased selection.
239
-
240
- ### `SelectBuilder`
241
-
242
- Chainable methods:
243
-
244
- - `.from(table)` - set the source table (only needed after `select()`)
245
- - `.pick(...keys)` - narrow to specific columns by name
246
- - `.where(expr)` - set the WHERE clause
247
- - `.orderBy(column, 'ASC' | 'DESC')` - append an ORDER BY entry
248
- - `.limit(n)` / `.offset(n)`
249
- - `.toSQL()` - compile to `{ sql, params, dependencies }`
250
- - `.dependencies()` - shortcut that returns just the dependencies
251
-
252
- ### `InferRow<Query>`
253
-
254
- Type helper that extracts the row type from a builder or compiled query.
@@ -1,99 +0,0 @@
1
- import type { Column } from './table'
2
- import { isColumn } from './table'
3
-
4
- export interface Expr {
5
- readonly kind: 'expr'
6
- readonly sql: string
7
- readonly params: unknown[]
8
- readonly refs: Column[]
9
- }
10
-
11
- function operand(v: any): Expr {
12
- if (isColumn(v)) {
13
- return {
14
- kind: 'expr',
15
- sql: `"${v._table}"."${v._name}"`,
16
- params: [],
17
- refs: [v],
18
- }
19
- }
20
- if (v && v.kind === 'expr')
21
- return v as Expr
22
- return { kind: 'expr', sql: '?', params: [v], refs: [] }
23
- }
24
-
25
- function binop(op: string) {
26
- return (a: Column | Expr | unknown, b: Column | Expr | unknown): Expr => {
27
- const x = operand(a)
28
- const y = operand(b)
29
- return {
30
- kind: 'expr',
31
- sql: `${x.sql} ${op} ${y.sql}`,
32
- params: [...x.params, ...y.params],
33
- refs: [...x.refs, ...y.refs],
34
- }
35
- }
36
- }
37
-
38
- export const eq = binop('=')
39
- export const ne = binop('<>')
40
- export const gt = binop('>')
41
- export const gte = binop('>=')
42
- export const lt = binop('<')
43
- export const lte = binop('<=')
44
- export const like = binop('LIKE')
45
-
46
- export function and(...parts: (Expr | undefined | false | null)[]): Expr {
47
- const list = parts.filter((p): p is Expr => !!p)
48
- if (list.length === 0)
49
- return { kind: 'expr', sql: '1=1', params: [], refs: [] }
50
- if (list.length === 1)
51
- return list[0]
52
- return {
53
- kind: 'expr',
54
- sql: list.map(p => `(${p.sql})`).join(' AND '),
55
- params: list.flatMap(p => p.params),
56
- refs: list.flatMap(p => p.refs),
57
- }
58
- }
59
-
60
- export function or(...parts: (Expr | undefined | false | null)[]): Expr {
61
- const list = parts.filter((p): p is Expr => !!p)
62
- if (list.length === 0)
63
- return { kind: 'expr', sql: '1=0', params: [], refs: [] }
64
- if (list.length === 1)
65
- return list[0]
66
- return {
67
- kind: 'expr',
68
- sql: list.map(p => `(${p.sql})`).join(' OR '),
69
- params: list.flatMap(p => p.params),
70
- refs: list.flatMap(p => p.refs),
71
- }
72
- }
73
-
74
- export function not(e: Expr): Expr {
75
- return { kind: 'expr', sql: `NOT (${e.sql})`, params: e.params, refs: e.refs }
76
- }
77
-
78
- export function sqlIsNull(col: Column | Expr): Expr {
79
- const x = operand(col)
80
- return { kind: 'expr', sql: `${x.sql} IS NULL`, params: x.params, refs: x.refs }
81
- }
82
-
83
- export function sqlIsNotNull(col: Column | Expr): Expr {
84
- const x = operand(col)
85
- return { kind: 'expr', sql: `${x.sql} IS NOT NULL`, params: x.params, refs: x.refs }
86
- }
87
-
88
- export function inArray(col: Column | Expr, values: unknown[]): Expr {
89
- const x = operand(col)
90
- if (values.length === 0)
91
- return { kind: 'expr', sql: '1=0', params: [], refs: x.refs }
92
- const placeholders = values.map(() => '?').join(', ')
93
- return {
94
- kind: 'expr',
95
- sql: `${x.sql} IN (${placeholders})`,
96
- params: [...x.params, ...values],
97
- refs: x.refs,
98
- }
99
- }
@@ -1,3 +0,0 @@
1
- export * from './expr'
2
- export * from './select'
3
- export * from './table'
@@ -1,144 +0,0 @@
1
- import type { Expect, IsEqual } from '../type-test'
2
- import type { InferRow } from './select'
3
- import { boolean, int, string } from '../schema'
4
- import { and, eq, gt, inArray, like, or } from './expr'
5
- import { from, select } from './select'
6
- import { table } from './table'
7
-
8
- const users = table('users', {
9
- id: int(),
10
- name: string(),
11
- email: string(),
12
- age: int(),
13
- active: boolean(),
14
- })
15
-
16
- describe('sql select', () => {
17
- it('selects all columns of a table', () => {
18
- const q = select().from(users).toSQL()
19
- expect(q.sql).toBe(
20
- 'SELECT "users"."id", "users"."name", "users"."email", "users"."age", "users"."active" FROM "users"',
21
- )
22
- expect(q.params).toEqual([])
23
- expect(q.dependencies).toEqual([
24
- {
25
- table: 'users',
26
- select: ['active', 'age', 'email', 'id', 'name'],
27
- where: [],
28
- orderBy: [],
29
- all: ['active', 'age', 'email', 'id', 'name'],
30
- },
31
- ])
32
- })
33
-
34
- it('selects specific columns with type inference', () => {
35
- const q = select({ id: users.id, name: users.name }).from(users)
36
- type Row = InferRow<typeof q>
37
- type _T = Expect<IsEqual<Row, { id: number, name: string }>>
38
-
39
- const c = q.toSQL()
40
- expect(c.sql).toBe('SELECT "users"."id", "users"."name" FROM "users"')
41
- expect(c.dependencies).toEqual([
42
- {
43
- table: 'users',
44
- select: ['id', 'name'],
45
- where: [],
46
- orderBy: [],
47
- all: ['id', 'name'],
48
- },
49
- ])
50
- })
51
-
52
- it('from() + pick() is a compact form with typed keys', () => {
53
- const q = from(users).pick('id', 'name').where(eq(users.email, 'x'))
54
- type Row = InferRow<typeof q>
55
- type _T = Expect<IsEqual<Row, { id: number, name: string }>>
56
-
57
- const c = q.toSQL()
58
- expect(c.sql).toBe(
59
- 'SELECT "users"."id", "users"."name" FROM "users" WHERE "users"."email" = ?',
60
- )
61
- expect(c.params).toEqual(['x'])
62
- expect(c.dependencies[0].select).toEqual(['id', 'name'])
63
- expect(c.dependencies[0].where).toEqual(['email'])
64
- })
65
-
66
- it('from() without pick returns the full row', () => {
67
- const q = from(users)
68
- type Row = InferRow<typeof q>
69
- type _T = Expect<IsEqual<Row, {
70
- id: number
71
- name: string
72
- email: string
73
- age: number
74
- active: boolean
75
- }>>
76
- expect(q.toSQL().sql).toBe(
77
- 'SELECT "users"."id", "users"."name", "users"."email", "users"."age", "users"."active" FROM "users"',
78
- )
79
- })
80
-
81
- it('supports aliases in selection', () => {
82
- const q = select({ uid: users.id }).from(users).toSQL()
83
- expect(q.sql).toBe('SELECT "users"."id" AS "uid" FROM "users"')
84
- })
85
-
86
- it('builds where with parameters', () => {
87
- const q = select()
88
- .from(users)
89
- .where(and(eq(users.active, true), gt(users.age, 18)))
90
- .toSQL()
91
- expect(q.sql).toContain('WHERE ("users"."active" = ?) AND ("users"."age" > ?)')
92
- expect(q.params).toEqual([true, 18])
93
- })
94
-
95
- it('separates select, where and orderBy dependencies', () => {
96
- const q = select({ id: users.id })
97
- .from(users)
98
- .where(eq(users.email, 'a@b.c'))
99
- .orderBy(users.age, 'DESC')
100
- .toSQL()
101
- const dep = q.dependencies[0]
102
- expect(dep.table).toBe('users')
103
- expect(dep.select).toEqual(['id'])
104
- expect(dep.where).toEqual(['email'])
105
- expect(dep.orderBy).toEqual(['age'])
106
- expect(dep.all).toEqual(['age', 'email', 'id'])
107
- })
108
-
109
- it('supports or, like, inArray', () => {
110
- const q = select()
111
- .from(users)
112
- .where(or(like(users.name, 'A%'), inArray(users.id, [1, 2, 3])))
113
- .toSQL()
114
- expect(q.sql).toContain('WHERE ("users"."name" LIKE ?) OR ("users"."id" IN (?, ?, ?))')
115
- expect(q.params).toEqual(['A%', 1, 2, 3])
116
- })
117
-
118
- it('handles orderBy, limit, offset and tracks orderBy deps', () => {
119
- const q = select({ id: users.id })
120
- .from(users)
121
- .orderBy(users.age, 'DESC')
122
- .limit(10)
123
- .offset(20)
124
- .toSQL()
125
- expect(q.sql).toBe(
126
- 'SELECT "users"."id" FROM "users" ORDER BY "users"."age" DESC LIMIT ? OFFSET ?',
127
- )
128
- expect(q.params).toEqual([10, 20])
129
- expect(q.dependencies[0].select).toEqual(['id'])
130
- expect(q.dependencies[0].orderBy).toEqual(['age'])
131
- expect(q.dependencies[0].all).toEqual(['age', 'id'])
132
- })
133
-
134
- it('dependencies() invalidation check helper', () => {
135
- const q = select({ id: users.id }).from(users).where(eq(users.email, 'x'))
136
- const deps = q.dependencies()
137
- const changedTable = 'users'
138
- const changedColumns = ['email']
139
- const affected = deps.some(d =>
140
- d.table === changedTable && d.all.some(c => changedColumns.includes(c)),
141
- )
142
- expect(affected).toBe(true)
143
- })
144
- })
@@ -1,207 +0,0 @@
1
- import type { Infer, Type } from '../schema'
2
- import type { Expr } from './expr'
3
- import type { Column, TableColumns, TableShape } from './table'
4
-
5
- export type RowFromTable<T> = T extends TableColumns<any, infer S>
6
- ? { [K in keyof S]: Infer<S[K]> }
7
- : never
8
-
9
- export type RowFromSelection<S> = {
10
- [K in keyof S]: S[K] extends Column<infer T> ? T : never
11
- }
12
-
13
- export interface QueryDependencies {
14
- readonly table: string
15
- readonly select: readonly string[]
16
- readonly where: readonly string[]
17
- readonly orderBy: readonly string[]
18
- readonly all: readonly string[]
19
- }
20
-
21
- export interface CompiledQuery<Row> {
22
- readonly sql: string
23
- readonly params: readonly unknown[]
24
- readonly dependencies: readonly QueryDependencies[]
25
- readonly __row?: Row
26
- }
27
-
28
- interface OrderByEntry {
29
- col: Column
30
- dir: 'ASC' | 'DESC'
31
- }
32
-
33
- interface SelectState {
34
- selection?: Record<string, Column>
35
- from?: TableColumns<any, any>
36
- where?: Expr
37
- orderBy: OrderByEntry[]
38
- limit?: number
39
- offset?: number
40
- }
41
-
42
- type ShapeRow<S> = { [K in keyof S]: S[K] extends Type<infer T> ? T : never }
43
-
44
- export class SelectBuilder<Row, Shape = unknown> {
45
- private _state: SelectState
46
-
47
- constructor(state: SelectState) {
48
- this._state = state
49
- }
50
-
51
- from<N extends string, S extends TableShape>(
52
- t: TableColumns<N, S>,
53
- ): SelectBuilder<Row extends void ? ShapeRow<S> : Row, S> {
54
- this._state.from = t
55
- return this as any
56
- }
57
-
58
- pick<K extends Extract<keyof Shape, string>>(
59
- ...keys: K[]
60
- ): SelectBuilder<{ [P in K]: Shape[P] extends Type<infer T> ? T : never }, Shape> {
61
- if (!this._state.from)
62
- throw new Error('pick: from() must be called first')
63
- const sel: Record<string, Column> = {}
64
- for (const k of keys) sel[k] = (this._state.from as any)[k]
65
- this._state.selection = sel
66
- return this as any
67
- }
68
-
69
- where(expr: Expr): this {
70
- this._state.where = expr
71
- return this
72
- }
73
-
74
- orderBy(col: Column, dir: 'ASC' | 'DESC' = 'ASC'): this {
75
- this._state.orderBy.push({ col, dir })
76
- return this
77
- }
78
-
79
- limit(n: number): this {
80
- this._state.limit = n
81
- return this
82
- }
83
-
84
- offset(n: number): this {
85
- this._state.offset = n
86
- return this
87
- }
88
-
89
- toSQL(): CompiledQuery<Row> {
90
- const s = this._state
91
- if (!s.from)
92
- throw new Error('select: from() is required')
93
-
94
- const tableName = s.from._name
95
- const params: unknown[] = []
96
- const selectRefs: Column[] = []
97
- const whereRefs: Column[] = []
98
- const orderByRefs: Column[] = []
99
-
100
- let cols: string
101
- if (s.selection) {
102
- const parts: string[] = []
103
- for (const [alias, col] of Object.entries(s.selection)) {
104
- selectRefs.push(col)
105
- const ident = `"${col._table}"."${col._name}"`
106
- parts.push(alias === col._name ? ident : `${ident} AS "${alias}"`)
107
- }
108
- cols = parts.join(', ')
109
- }
110
- else {
111
- const shape = s.from._shape
112
- const names = Object.keys(shape)
113
- for (const n of names) selectRefs.push((s.from as any)[n])
114
- cols = names.map(n => `"${tableName}"."${n}"`).join(', ')
115
- }
116
-
117
- let sql = `SELECT ${cols} FROM "${tableName}"`
118
-
119
- if (s.where) {
120
- sql += ` WHERE ${s.where.sql}`
121
- params.push(...s.where.params)
122
- whereRefs.push(...s.where.refs)
123
- }
124
-
125
- if (s.orderBy.length) {
126
- const parts = s.orderBy.map((o) => {
127
- orderByRefs.push(o.col)
128
- return `"${o.col._table}"."${o.col._name}" ${o.dir}`
129
- })
130
- sql += ` ORDER BY ${parts.join(', ')}`
131
- }
132
-
133
- if (s.limit != null) {
134
- sql += ` LIMIT ?`
135
- params.push(s.limit)
136
- }
137
-
138
- if (s.offset != null) {
139
- sql += ` OFFSET ?`
140
- params.push(s.offset)
141
- }
142
-
143
- return {
144
- sql,
145
- params,
146
- dependencies: collectDependencies(selectRefs, whereRefs, orderByRefs),
147
- __row: undefined as any,
148
- }
149
- }
150
-
151
- dependencies(): readonly QueryDependencies[] {
152
- return this.toSQL().dependencies
153
- }
154
- }
155
-
156
- function collectDependencies(
157
- selectRefs: Column[],
158
- whereRefs: Column[],
159
- orderByRefs: Column[],
160
- ): QueryDependencies[] {
161
- interface Buckets {
162
- select: Set<string>
163
- where: Set<string>
164
- orderBy: Set<string>
165
- }
166
- const map = new Map<string, Buckets>()
167
- const bucket = (table: string): Buckets => {
168
- let b = map.get(table)
169
- if (!b) {
170
- b = { select: new Set(), where: new Set(), orderBy: new Set() }
171
- map.set(table, b)
172
- }
173
- return b
174
- }
175
- for (const c of selectRefs) bucket(c._table).select.add(c._name)
176
- for (const c of whereRefs) bucket(c._table).where.add(c._name)
177
- for (const c of orderByRefs) bucket(c._table).orderBy.add(c._name)
178
-
179
- return Array.from(map, ([table, b]) => {
180
- const all = new Set<string>([...b.select, ...b.where, ...b.orderBy])
181
- return {
182
- table,
183
- select: Array.from(b.select).sort(),
184
- where: Array.from(b.where).sort(),
185
- orderBy: Array.from(b.orderBy).sort(),
186
- all: Array.from(all).sort(),
187
- }
188
- })
189
- }
190
-
191
- export function select(): SelectBuilder<void>
192
- export function select<S extends Record<string, Column<any>>>(
193
- selection: S,
194
- ): SelectBuilder<RowFromSelection<S>>
195
- export function select(selection?: Record<string, Column>): SelectBuilder<any> {
196
- return new SelectBuilder({ selection, orderBy: [] })
197
- }
198
-
199
- export function from<N extends string, S extends TableShape>(
200
- t: TableColumns<N, S>,
201
- ): SelectBuilder<ShapeRow<S>, S> {
202
- return new SelectBuilder<ShapeRow<S>, S>({ from: t, orderBy: [] })
203
- }
204
-
205
- export type InferRow<Q> = Q extends SelectBuilder<infer R>
206
- ? R
207
- : Q extends CompiledQuery<infer R> ? R : never
@@ -1,36 +0,0 @@
1
- import type { Infer, Type } from '../schema'
2
-
3
- export interface Column<T = unknown> {
4
- readonly _table: string
5
- readonly _name: string
6
- readonly _type: Type<T>
7
- readonly __row?: T
8
- }
9
-
10
- export type TableShape = Record<string, Type<any>>
11
-
12
- export type TableColumns<N extends string, S extends TableShape> = {
13
- readonly [K in keyof S & string]: Column<Infer<S[K]>>
14
- } & {
15
- readonly _name: N
16
- readonly _shape: S
17
- }
18
-
19
- export function table<N extends string, S extends TableShape>(
20
- name: N,
21
- shape: S,
22
- ): TableColumns<N, S> {
23
- const out: any = { _name: name, _shape: shape }
24
- for (const key of Object.keys(shape)) {
25
- out[key] = {
26
- _table: name,
27
- _name: key,
28
- _type: shape[key],
29
- }
30
- }
31
- return out
32
- }
33
-
34
- export function isColumn(v: any): v is Column {
35
- return !!v && typeof v._table === 'string' && typeof v._name === 'string' && v._type
36
- }