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 +22 -25
- package/src/common/bin/lib0/decoding.spec.ts +84 -1
- package/src/common/bin/lib0/encoding.spec.ts +109 -1
- package/src/common/bin/lib0/string.spec.ts +45 -1
- package/src/common/crypto/crypto.spec.ts +7 -0
- package/src/common/crypto/xaes.spec.ts +48 -0
- package/src/common/data/deep.spec.ts +45 -0
- package/src/common/data/regexp.spec.ts +30 -0
- package/src/common/schema/index.ts +0 -1
- package/src/common/schema/_sandbox/sandbox-inherit.ts +0 -13
- package/src/common/schema/_sandbox/sandbox.ts +0 -42
- package/src/common/schema/_sandbox/sandbox.xspec.ts +0 -45
- package/src/common/schema/sql/README.md +0 -254
- package/src/common/schema/sql/expr.ts +0 -99
- package/src/common/schema/sql/index.ts +0 -3
- package/src/common/schema/sql/select.spec.ts +0 -144
- package/src/common/schema/sql/select.ts +0 -207
- package/src/common/schema/sql/table.ts +0 -36
package/package.json
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zeed",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
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
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
+
})
|
|
@@ -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,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
|
-
}
|