@weft-js/db 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/package.json +13 -4
- package/plugins/dynamodb.mjs +10 -2
- package/plugins/firestore.mjs +10 -2
- package/plugins/kysely.mjs +10 -2
- package/plugins/mongo.mjs +10 -2
- package/plugins/native.mjs +16 -4
- package/plugins/prisma-client.mjs +29 -6
- package/plugins/prisma.mjs +10 -2
- package/plugins/sql.mjs +10 -2
- package/src/driver.ts +11 -4
- package/src/dynamodb.ts +165 -49
- package/src/firestore.ts +89 -31
- package/src/kysely.ts +108 -32
- package/src/mongo.ts +125 -30
- package/src/native/auth.ts +37 -9
- package/src/native/index.ts +36 -23
- package/src/native/mysql.ts +22 -5
- package/src/native/postgres.ts +28 -10
- package/src/native/sqlite.ts +20 -6
- package/src/prisma-client.ts +53 -24
- package/src/prisma.ts +102 -28
- package/src/sql.ts +141 -43
package/src/sql.ts
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
// A list source from bound SQL: `columns` is required, identifiers come only from it, and every value
|
|
2
2
|
// is a parameter. `target` is anything with `query(text, params)`: `ctx.db` from `@weft-js/db/plugin-native`, or a driver.
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
likeOf,
|
|
6
|
+
pageOf,
|
|
7
|
+
plan,
|
|
8
|
+
rowOf,
|
|
9
|
+
storedId,
|
|
10
|
+
written,
|
|
11
|
+
type DbAdapter,
|
|
12
|
+
type ListParams,
|
|
13
|
+
type ListSource,
|
|
14
|
+
type SourceSpec,
|
|
15
|
+
} from '@weft-js/start/list'
|
|
5
16
|
import { StartError } from '@weft-js/start/data'
|
|
6
17
|
|
|
7
18
|
/** Whatever runs a bound statement, and says which dialect it speaks. */
|
|
@@ -12,20 +23,101 @@ export interface SqlTarget {
|
|
|
12
23
|
}
|
|
13
24
|
|
|
14
25
|
/** A source's allowlist, and the table it covers. */
|
|
15
|
-
export interface SqlSpec extends SourceSpec {
|
|
26
|
+
export interface SqlSpec extends SourceSpec {
|
|
27
|
+
table: string
|
|
28
|
+
}
|
|
16
29
|
|
|
17
30
|
const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
18
31
|
|
|
19
32
|
function ident(dialect: string) {
|
|
20
33
|
const q = dialect === 'mysql' ? '`' : '"'
|
|
21
34
|
return (name: string) => {
|
|
22
|
-
if (!IDENT.test(name))
|
|
35
|
+
if (!IDENT.test(name))
|
|
36
|
+
throw new StartError(
|
|
37
|
+
'Q002',
|
|
38
|
+
`\`${name}\` is not a plain identifier, so it can't go in SQL`,
|
|
39
|
+
'use a column name',
|
|
40
|
+
)
|
|
23
41
|
return `${q}${name}${q}`
|
|
24
42
|
}
|
|
25
43
|
}
|
|
26
44
|
|
|
45
|
+
function listSql<Row>({
|
|
46
|
+
target,
|
|
47
|
+
spec,
|
|
48
|
+
q,
|
|
49
|
+
bind,
|
|
50
|
+
read,
|
|
51
|
+
table,
|
|
52
|
+
select,
|
|
53
|
+
idColumn,
|
|
54
|
+
}: {
|
|
55
|
+
target: SqlTarget
|
|
56
|
+
spec: SqlSpec
|
|
57
|
+
q: (name: string) => string
|
|
58
|
+
bind: (params: unknown[]) => (value: unknown) => string
|
|
59
|
+
read: (text: string, params: unknown[]) => Promise<Row[]>
|
|
60
|
+
table: string
|
|
61
|
+
select: string
|
|
62
|
+
idColumn: string
|
|
63
|
+
}): ListSource<Row>['list'] {
|
|
64
|
+
return async function list(p: ListParams) {
|
|
65
|
+
const qp = plan(spec, p)
|
|
66
|
+
const params: unknown[] = []
|
|
67
|
+
const put = bind(params)
|
|
68
|
+
const parts = qp.where.map((w) => {
|
|
69
|
+
const some = w.values.filter((v) => v !== null)
|
|
70
|
+
const hit =
|
|
71
|
+
some.length === 1
|
|
72
|
+
? `${q(w.column)} = ${put(some[0])}`
|
|
73
|
+
: some.length
|
|
74
|
+
? `${q(w.column)} in (${some.map(put).join(', ')})`
|
|
75
|
+
: ''
|
|
76
|
+
return w.values.includes(null) ? `(${hit ? `${hit} or ` : ''}${q(w.column)} is null)` : hit
|
|
77
|
+
})
|
|
78
|
+
if (qp.search) {
|
|
79
|
+
const text = likeOf(qp.search.text)
|
|
80
|
+
parts.push(
|
|
81
|
+
`(${qp.search.columns.map((c) => `lower(${q(c)}) like ${put(text)} escape '!'`).join(' or ')})`,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
const filtered = [...parts],
|
|
85
|
+
mark = params.length // what the count binds: the keyset isn't part of it
|
|
86
|
+
const cmp = qp.sort?.dir === 'desc' ? '<' : '>'
|
|
87
|
+
if (p.after) {
|
|
88
|
+
const [value, id] = p.after
|
|
89
|
+
const col = qp.sort && q(qp.sort.column)
|
|
90
|
+
parts.push(
|
|
91
|
+
col
|
|
92
|
+
? `(${col} ${cmp} ${put(value)} or (${col} = ${put(value)} and ${idColumn} ${cmp} ${put(id)}))`
|
|
93
|
+
: `${idColumn} > ${put(id)}`,
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
const where = (conditions: string[]) =>
|
|
97
|
+
conditions.length ? ` where ${conditions.join(' and ')}` : ''
|
|
98
|
+
const dir = qp.sort?.dir === 'desc' ? 'desc' : 'asc'
|
|
99
|
+
const order = qp.sort ? `${q(qp.sort.column)} ${dir}, ${idColumn} ${dir}` : `${idColumn} asc`
|
|
100
|
+
const cursor = p.mode === 'cursor'
|
|
101
|
+
const counted = params.slice(0, mark)
|
|
102
|
+
const rows = await read(
|
|
103
|
+
`select ${select} from ${table}${where(parts)} order by ${order} limit ${Math.trunc(cursor ? p.limit + 1 : p.limit)} offset ${Math.trunc(p.offset)}`,
|
|
104
|
+
params,
|
|
105
|
+
)
|
|
106
|
+
const total = cursor
|
|
107
|
+
? undefined
|
|
108
|
+
: Number(
|
|
109
|
+
(await target.query(`select count(*) as n from ${table}${where(filtered)}`, counted))[0]!
|
|
110
|
+
.n,
|
|
111
|
+
)
|
|
112
|
+
return pageOf(rows, p, { sort: qp.sort?.field, id: qp.id.field }, total)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
27
116
|
/** A `ListSource` over one table, as bound SQL. */
|
|
28
|
-
export function sqlSource<Row = Record<string, unknown>>(
|
|
117
|
+
export function sqlSource<Row = Record<string, unknown>>(
|
|
118
|
+
target: SqlTarget,
|
|
119
|
+
spec: SqlSpec,
|
|
120
|
+
): ListSource<Row> {
|
|
29
121
|
const dialect = target.dialect ?? target.$weft?.dialect ?? 'postgresql'
|
|
30
122
|
const q = ident(dialect)
|
|
31
123
|
for (const c of [spec.table, ...Object.values(spec.columns)]) q(c)
|
|
@@ -37,52 +129,47 @@ export function sqlSource<Row = Record<string, unknown>>(target: SqlTarget, spec
|
|
|
37
129
|
params.push(v)
|
|
38
130
|
return dialect === 'postgresql' ? `$${params.length}` : '?'
|
|
39
131
|
}
|
|
40
|
-
const read = async (text: string, params: unknown[]) =>
|
|
41
|
-
|
|
132
|
+
const read = async (text: string, params: unknown[]) =>
|
|
133
|
+
(await target.query(text, params)).map(reading)
|
|
134
|
+
const byId = (id: unknown) => {
|
|
135
|
+
const params: unknown[] = []
|
|
136
|
+
return read(
|
|
137
|
+
`select ${select} from ${table} where ${idColumn} = ${bind(params)(id)}`,
|
|
138
|
+
params,
|
|
139
|
+
).then((r) => r[0] ?? null)
|
|
140
|
+
}
|
|
42
141
|
return {
|
|
43
|
-
|
|
44
|
-
const qp = plan(spec, p)
|
|
45
|
-
const params: unknown[] = []
|
|
46
|
-
const put = bind(params)
|
|
47
|
-
const parts = qp.where.map(w => {
|
|
48
|
-
const some = w.values.filter(v => v !== null)
|
|
49
|
-
const hit = some.length === 1 ? `${q(w.column)} = ${put(some[0])}` : some.length ? `${q(w.column)} in (${some.map(put).join(', ')})` : ''
|
|
50
|
-
return w.values.includes(null) ? `(${hit ? `${hit} or ` : ''}${q(w.column)} is null)` : hit
|
|
51
|
-
})
|
|
52
|
-
if (qp.search) {
|
|
53
|
-
const text = likeOf(qp.search.text)
|
|
54
|
-
parts.push(`(${qp.search.columns.map(c => `lower(${q(c)}) like ${put(text)} escape '!'`).join(' or ')})`)
|
|
55
|
-
}
|
|
56
|
-
const filtered = [...parts], mark = params.length // what the count binds: the keyset isn't part of it
|
|
57
|
-
const cmp = qp.sort?.dir === 'desc' ? '<' : '>'
|
|
58
|
-
if (p.after) {
|
|
59
|
-
const [value, id] = p.after
|
|
60
|
-
const col = qp.sort && q(qp.sort.column)
|
|
61
|
-
parts.push(col ? `(${col} ${cmp} ${put(value)} or (${col} = ${put(value)} and ${idColumn} ${cmp} ${put(id)}))` : `${idColumn} > ${put(id)}`)
|
|
62
|
-
}
|
|
63
|
-
const where = (list: string[]) => (list.length ? ` where ${list.join(' and ')}` : '')
|
|
64
|
-
const dir = qp.sort?.dir === 'desc' ? 'desc' : 'asc'
|
|
65
|
-
const order = qp.sort ? `${q(qp.sort.column)} ${dir}, ${idColumn} ${dir}` : `${idColumn} asc`
|
|
66
|
-
const cursor = p.mode === 'cursor'
|
|
67
|
-
const counted = params.slice(0, mark)
|
|
68
|
-
const rows = await read(`select ${select} from ${table}${where(parts)} order by ${order} limit ${Math.trunc(cursor ? p.limit + 1 : p.limit)} offset ${Math.trunc(p.offset)}`, params)
|
|
69
|
-
const total = cursor ? undefined : Number((await target.query(`select count(*) as n from ${table}${where(filtered)}`, counted))[0]!.n)
|
|
70
|
-
return pageOf(rows, p, { sort: qp.sort?.field, id: qp.id.field }, total)
|
|
71
|
-
},
|
|
142
|
+
list: listSql({ target, spec, q, bind, read, table, select, idColumn }),
|
|
72
143
|
get: byId,
|
|
73
144
|
async create(data) {
|
|
74
145
|
const row = written(spec, data, true)
|
|
75
146
|
const id = data[spec.id ?? 'id']
|
|
76
|
-
if (id === undefined)
|
|
77
|
-
|
|
78
|
-
|
|
147
|
+
if (id === undefined)
|
|
148
|
+
throw new StartError(
|
|
149
|
+
'Q002',
|
|
150
|
+
"`create` needs the id: a generated one can't be read back on every dialect",
|
|
151
|
+
'generate it before the insert',
|
|
152
|
+
)
|
|
153
|
+
const keys = Object.keys(row),
|
|
154
|
+
params: unknown[] = [],
|
|
155
|
+
put = bind(params)
|
|
156
|
+
await target.query(
|
|
157
|
+
`insert into ${table} (${keys.map(q).join(', ')}) values (${keys.map((k) => put(row[k])).join(', ')})`,
|
|
158
|
+
params,
|
|
159
|
+
)
|
|
79
160
|
return (await byId(id))!
|
|
80
161
|
},
|
|
81
162
|
async update(id, patch) {
|
|
82
|
-
const row = written(spec, patch),
|
|
83
|
-
|
|
163
|
+
const row = written(spec, patch),
|
|
164
|
+
params: unknown[] = [],
|
|
165
|
+
put = bind(params)
|
|
166
|
+
const sets = Object.keys(row).map((k) => `${q(k)} = ${put(row[k])}`)
|
|
84
167
|
if (!(await byId(id))) return null
|
|
85
|
-
if (sets.length)
|
|
168
|
+
if (sets.length)
|
|
169
|
+
await target.query(
|
|
170
|
+
`update ${table} set ${sets.join(', ')} where ${idColumn} = ${put(id)}`,
|
|
171
|
+
params,
|
|
172
|
+
)
|
|
86
173
|
return byId(id)
|
|
87
174
|
},
|
|
88
175
|
async remove(id) {
|
|
@@ -95,13 +182,24 @@ export function sqlSource<Row = Record<string, unknown>>(target: SqlTarget, spec
|
|
|
95
182
|
}
|
|
96
183
|
|
|
97
184
|
/** `db.adapter@1` over bound SQL. */
|
|
98
|
-
export const adapter: DbAdapter<SqlTarget> = {
|
|
185
|
+
export const adapter: DbAdapter<SqlTarget> = {
|
|
186
|
+
name: 'sql',
|
|
187
|
+
source: (t, spec) => sqlSource(t, spec as SqlSpec),
|
|
188
|
+
}
|
|
99
189
|
|
|
100
190
|
/**
|
|
101
191
|
* A trigger so writes from anywhere (cron, psql, another service) reach `.live` queries, on the postgres bus: tags, never
|
|
102
192
|
* rows. Run the text once in a migration.
|
|
103
193
|
*/
|
|
104
|
-
export function postgresTrigger({
|
|
194
|
+
export function postgresTrigger({
|
|
195
|
+
table,
|
|
196
|
+
id = 'id',
|
|
197
|
+
tag = table,
|
|
198
|
+
}: {
|
|
199
|
+
table: string
|
|
200
|
+
id?: string
|
|
201
|
+
tag?: string
|
|
202
|
+
}): string {
|
|
105
203
|
const q = ident('postgresql')
|
|
106
204
|
if (!/^[\w:-]+$/.test(tag)) throw new StartError('Q002', `\`${tag}\` can't be a tag in a trigger`)
|
|
107
205
|
const fn = `weft_poke_${table}`
|