@weft-js/db 0.1.1 → 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/firestore.ts
CHANGED
|
@@ -2,13 +2,29 @@
|
|
|
2
2
|
// document id is the row's id. Firestore can't search inside a string, and a range is one field at a time, so `search`
|
|
3
3
|
// is a prefix on the first search column, and can't be combined with another sort.
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
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'
|
|
6
16
|
import { StartError } from '@weft-js/start/data'
|
|
7
17
|
|
|
8
|
-
const NAME = '__name__',
|
|
18
|
+
const NAME = '__name__',
|
|
19
|
+
HIGH = '',
|
|
20
|
+
IN_MAX = 30
|
|
9
21
|
|
|
10
22
|
/** One document, read. */
|
|
11
|
-
export interface FirestoreDoc {
|
|
23
|
+
export interface FirestoreDoc {
|
|
24
|
+
id: string
|
|
25
|
+
exists: boolean
|
|
26
|
+
data(): object | undefined
|
|
27
|
+
}
|
|
12
28
|
|
|
13
29
|
/** Where one document lives. */
|
|
14
30
|
export interface FirestoreRef {
|
|
@@ -35,39 +51,78 @@ export interface FirestoreCollection {
|
|
|
35
51
|
doc(id?: string): FirestoreRef
|
|
36
52
|
}
|
|
37
53
|
|
|
54
|
+
type FirestoreList<Row> = {
|
|
55
|
+
collection: FirestoreCollection
|
|
56
|
+
spec: SourceSpec
|
|
57
|
+
stored: string[]
|
|
58
|
+
idColumn: string
|
|
59
|
+
idField: string
|
|
60
|
+
row: (doc: FirestoreDoc) => Row
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function listRows<Row>(p: ListParams, context: FirestoreList<Row>) {
|
|
64
|
+
const { collection, spec, stored, idColumn, idField, row } = context
|
|
65
|
+
const q = plan(spec, p)
|
|
66
|
+
let b = collection.select(...stored)
|
|
67
|
+
for (const w of q.where) {
|
|
68
|
+
if (w.values.length > IN_MAX)
|
|
69
|
+
throw new StartError('Q002', `Firestore takes ${IN_MAX} values in a filter`, 'ask for fewer')
|
|
70
|
+
b =
|
|
71
|
+
w.values.length === 1
|
|
72
|
+
? b.where(w.column, '==' as never, w.values[0])
|
|
73
|
+
: b.where(w.column, 'in' as never, w.values)
|
|
74
|
+
}
|
|
75
|
+
let first = q.sort?.column === idColumn ? null : (q.sort?.column ?? null)
|
|
76
|
+
const dir = q.sort?.dir ?? 'asc'
|
|
77
|
+
if (q.search) {
|
|
78
|
+
const field = q.search.columns[0]!
|
|
79
|
+
if (first && first !== field)
|
|
80
|
+
throw new StartError(
|
|
81
|
+
'Q002',
|
|
82
|
+
"Firestore can't search one field and sort by another",
|
|
83
|
+
'drop one',
|
|
84
|
+
)
|
|
85
|
+
first = field
|
|
86
|
+
b = b
|
|
87
|
+
.where(field, '>=' as never, q.search.text)
|
|
88
|
+
.where(field, '<=' as never, q.search.text + HIGH)
|
|
89
|
+
}
|
|
90
|
+
const counted = b
|
|
91
|
+
if (first) b = b.orderBy(first, dir)
|
|
92
|
+
b = b.orderBy(NAME, dir)
|
|
93
|
+
if (p.after) b = first ? b.startAfter(p.after[0], p.after[1]) : b.startAfter(p.after[1])
|
|
94
|
+
const cursor = p.mode === 'cursor'
|
|
95
|
+
if (p.offset) b = b.offset(p.offset)
|
|
96
|
+
const rows = (await b.limit(cursor ? p.limit + 1 : p.limit).get()).docs.map(row)
|
|
97
|
+
const total = cursor ? undefined : (await counted.count().get()).data().count
|
|
98
|
+
return pageOf(
|
|
99
|
+
rows,
|
|
100
|
+
p,
|
|
101
|
+
{
|
|
102
|
+
sort:
|
|
103
|
+
q.sort?.field ??
|
|
104
|
+
(first ? Object.keys(spec.columns).find((k) => spec.columns[k] === first) : undefined),
|
|
105
|
+
id: idField,
|
|
106
|
+
},
|
|
107
|
+
total,
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
38
111
|
/** A `ListSource` over one collection, keyed by document id. */
|
|
39
|
-
export function firestoreSource<Row = Record<string, unknown>>(
|
|
112
|
+
export function firestoreSource<Row = Record<string, unknown>>(
|
|
113
|
+
collection: FirestoreCollection,
|
|
114
|
+
spec: SourceSpec,
|
|
115
|
+
): ListSource<Row> {
|
|
40
116
|
const reading = rowOf<Row>(spec)
|
|
41
|
-
const idField = spec.id ?? 'id',
|
|
42
|
-
|
|
117
|
+
const idField = spec.id ?? 'id',
|
|
118
|
+
idColumn = storedId(spec)
|
|
119
|
+
const stored = Object.values(spec.columns).filter((c) => c !== idColumn)
|
|
43
120
|
const row = (doc: FirestoreDoc) => reading({ [idColumn]: doc.id, ...doc.data() })
|
|
44
121
|
const at = (id: unknown) => collection.doc(String(id))
|
|
45
122
|
return {
|
|
46
123
|
limits: { search: 'prefix' },
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
let b = collection.select(...stored)
|
|
50
|
-
for (const w of q.where) {
|
|
51
|
-
if (w.values.length > IN_MAX) throw new StartError('Q002', `Firestore takes ${IN_MAX} values in a filter`, 'ask for fewer')
|
|
52
|
-
b = w.values.length === 1 ? b.where(w.column, '==' as never, w.values[0]) : b.where(w.column, 'in' as never, w.values)
|
|
53
|
-
}
|
|
54
|
-
let first = q.sort?.column === idColumn ? null : q.sort?.column ?? null
|
|
55
|
-
const dir = q.sort?.dir ?? 'asc'
|
|
56
|
-
if (q.search) {
|
|
57
|
-
const field = q.search.columns[0]!
|
|
58
|
-
if (first && first !== field) throw new StartError('Q002', 'Firestore can\'t search one field and sort by another', 'drop one')
|
|
59
|
-
first = field
|
|
60
|
-
b = b.where(field, '>=' as never, q.search.text).where(field, '<=' as never, q.search.text + HIGH)
|
|
61
|
-
}
|
|
62
|
-
const counted = b
|
|
63
|
-
if (first) b = b.orderBy(first, dir)
|
|
64
|
-
b = b.orderBy(NAME, dir)
|
|
65
|
-
if (p.after) b = first ? b.startAfter(p.after[0], p.after[1]) : b.startAfter(p.after[1])
|
|
66
|
-
const cursor = p.mode === 'cursor'
|
|
67
|
-
if (p.offset) b = b.offset(p.offset)
|
|
68
|
-
const rows = (await b.limit(cursor ? p.limit + 1 : p.limit).get()).docs.map(row)
|
|
69
|
-
const total = cursor ? undefined : (await counted.count().get()).data().count
|
|
70
|
-
return pageOf(rows, p, { sort: q.sort?.field ?? (first ? Object.keys(spec.columns).find(k => spec.columns[k] === first) : undefined), id: idField }, total)
|
|
124
|
+
list(p: ListParams) {
|
|
125
|
+
return listRows(p, { collection, spec, stored, idColumn, idField, row })
|
|
71
126
|
},
|
|
72
127
|
async get(id) {
|
|
73
128
|
const doc = await at(id).get()
|
|
@@ -96,4 +151,7 @@ export function firestoreSource<Row = Record<string, unknown>>(collection: Fires
|
|
|
96
151
|
}
|
|
97
152
|
|
|
98
153
|
/** `db.adapter@1` over a Firestore collection. */
|
|
99
|
-
export const adapter: DbAdapter<FirestoreCollection> = {
|
|
154
|
+
export const adapter: DbAdapter<FirestoreCollection> = {
|
|
155
|
+
name: 'firestore',
|
|
156
|
+
source: (t, spec) => firestoreSource(t, spec),
|
|
157
|
+
}
|
package/src/kysely.ts
CHANGED
|
@@ -2,7 +2,18 @@
|
|
|
2
2
|
// builder gets them from the spec, never from a request.
|
|
3
3
|
|
|
4
4
|
import type { ExpressionBuilder, Kysely, SelectQueryBuilder } from 'kysely'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
likeOf,
|
|
7
|
+
pageOf,
|
|
8
|
+
plan,
|
|
9
|
+
rowOf,
|
|
10
|
+
storedId,
|
|
11
|
+
written,
|
|
12
|
+
type DbAdapter,
|
|
13
|
+
type ListParams,
|
|
14
|
+
type ListSource,
|
|
15
|
+
type SourceSpec,
|
|
16
|
+
} from '@weft-js/start/list'
|
|
6
17
|
import { StartError } from '@weft-js/start/data'
|
|
7
18
|
import { driver } from './driver'
|
|
8
19
|
|
|
@@ -18,8 +29,78 @@ export interface KyselySpec extends SourceSpec {
|
|
|
18
29
|
type Tables = Record<string, Record<string, unknown>>
|
|
19
30
|
type Eb = ExpressionBuilder<Tables, string>
|
|
20
31
|
|
|
32
|
+
type KyselyList<Row> = {
|
|
33
|
+
db: Kysely<Tables>
|
|
34
|
+
spec: KyselySpec
|
|
35
|
+
table: string
|
|
36
|
+
columns: string[]
|
|
37
|
+
idColumn: string
|
|
38
|
+
reading: (row: object) => Row
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function listRows<Row>(p: ListParams, context: KyselyList<Row>) {
|
|
42
|
+
const { db, spec, table, columns, idColumn, reading } = context
|
|
43
|
+
const q = plan(spec, p)
|
|
44
|
+
const filtered = <O>(from: SelectQueryBuilder<Tables, string, O>) => {
|
|
45
|
+
let b = from
|
|
46
|
+
for (const w of q.where) {
|
|
47
|
+
const some = w.values.filter((v) => v !== null)
|
|
48
|
+
b = b.where((eb: Eb) =>
|
|
49
|
+
eb.or([
|
|
50
|
+
...(some.length ? [eb(w.column, 'in', some)] : []),
|
|
51
|
+
...(w.values.includes(null) ? [eb(w.column, 'is', null)] : []),
|
|
52
|
+
]),
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
if (q.search) {
|
|
56
|
+
const like = likeOf(q.search.text)
|
|
57
|
+
b = b.where((eb: Eb) =>
|
|
58
|
+
eb.or(
|
|
59
|
+
q.search!.columns.map((c) => sql<boolean>`lower(${sql.ref(c)}) like ${like} escape '!'`),
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
return b
|
|
64
|
+
}
|
|
65
|
+
let b = filtered(db.selectFrom(table).select(columns))
|
|
66
|
+
const dir = q.sort?.dir === 'desc' ? 'desc' : 'asc',
|
|
67
|
+
cmp = dir === 'desc' ? '<' : '>'
|
|
68
|
+
if (p.after) {
|
|
69
|
+
const [value, id] = p.after
|
|
70
|
+
b = b.where((eb: Eb) =>
|
|
71
|
+
q.sort
|
|
72
|
+
? eb.or([
|
|
73
|
+
eb(q.sort.column, cmp, value),
|
|
74
|
+
eb.and([eb(q.sort.column, '=', value), eb(idColumn, cmp, id)]),
|
|
75
|
+
])
|
|
76
|
+
: eb(idColumn, '>', id),
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
b = q.sort ? b.orderBy(q.sort.column, dir).orderBy(idColumn, dir) : b.orderBy(idColumn, 'asc')
|
|
80
|
+
const cursor = p.mode === 'cursor'
|
|
81
|
+
const rows = (
|
|
82
|
+
await b
|
|
83
|
+
.limit(cursor ? p.limit + 1 : p.limit)
|
|
84
|
+
.offset(p.offset)
|
|
85
|
+
.execute()
|
|
86
|
+
).map(reading)
|
|
87
|
+
const total = cursor
|
|
88
|
+
? undefined
|
|
89
|
+
: Number(
|
|
90
|
+
(
|
|
91
|
+
await filtered(
|
|
92
|
+
db.selectFrom(table).select(sql<unknown>`count(*)`.as('n')),
|
|
93
|
+
).executeTakeFirstOrThrow()
|
|
94
|
+
).n,
|
|
95
|
+
)
|
|
96
|
+
return pageOf(rows, p, { sort: q.sort?.field, id: q.id.field }, total)
|
|
97
|
+
}
|
|
98
|
+
|
|
21
99
|
/** A `ListSource` over one table of any `Kysely<DB>`. */
|
|
22
|
-
export function kyselySource<Row = Record<string, unknown>, DB = unknown>(
|
|
100
|
+
export function kyselySource<Row = Record<string, unknown>, DB = unknown>(
|
|
101
|
+
handle: Kysely<DB>,
|
|
102
|
+
spec: KyselySpec,
|
|
103
|
+
): ListSource<Row> {
|
|
23
104
|
const db = handle as unknown as Kysely<Tables>
|
|
24
105
|
const { table, returning = true } = spec
|
|
25
106
|
const columns = Object.values(spec.columns)
|
|
@@ -30,38 +111,23 @@ export function kyselySource<Row = Record<string, unknown>, DB = unknown>(handle
|
|
|
30
111
|
return r ? reading(r) : null
|
|
31
112
|
}
|
|
32
113
|
return {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const filtered = <O>(from: SelectQueryBuilder<Tables, string, O>) => {
|
|
36
|
-
let b = from
|
|
37
|
-
for (const w of q.where) {
|
|
38
|
-
const some = w.values.filter(v => v !== null)
|
|
39
|
-
b = b.where((eb: Eb) => eb.or([...(some.length ? [eb(w.column, 'in', some)] : []), ...(w.values.includes(null) ? [eb(w.column, 'is', null)] : [])]))
|
|
40
|
-
}
|
|
41
|
-
if (q.search) {
|
|
42
|
-
const like = likeOf(q.search.text)
|
|
43
|
-
b = b.where((eb: Eb) => eb.or(q.search!.columns.map(c => sql<boolean>`lower(${sql.ref(c)}) like ${like} escape '!'`)))
|
|
44
|
-
}
|
|
45
|
-
return b
|
|
46
|
-
}
|
|
47
|
-
let b = filtered(db.selectFrom(table).select(columns))
|
|
48
|
-
const dir = q.sort?.dir === 'desc' ? 'desc' : 'asc', cmp = dir === 'desc' ? '<' : '>'
|
|
49
|
-
if (p.after) {
|
|
50
|
-
const [value, id] = p.after
|
|
51
|
-
b = b.where((eb: Eb) => (q.sort ? eb.or([eb(q.sort.column, cmp, value), eb.and([eb(q.sort.column, '=', value), eb(idColumn, cmp, id)])]) : eb(idColumn, '>', id)))
|
|
52
|
-
}
|
|
53
|
-
b = q.sort ? b.orderBy(q.sort.column, dir).orderBy(idColumn, dir) : b.orderBy(idColumn, 'asc')
|
|
54
|
-
const cursor = p.mode === 'cursor'
|
|
55
|
-
const rows = (await b.limit(cursor ? p.limit + 1 : p.limit).offset(p.offset).execute()).map(reading)
|
|
56
|
-
const total = cursor ? undefined : Number((await filtered(db.selectFrom(table).select(sql<unknown>`count(*)`.as('n'))).executeTakeFirstOrThrow()).n)
|
|
57
|
-
return pageOf(rows, p, { sort: q.sort?.field, id: q.id.field }, total)
|
|
114
|
+
list(p: ListParams) {
|
|
115
|
+
return listRows(p, { db, spec, table, columns, idColumn, reading })
|
|
58
116
|
},
|
|
59
117
|
get: byId,
|
|
60
118
|
async create(data) {
|
|
61
119
|
const row = written(spec, data, true)
|
|
62
120
|
const id = row[idColumn]
|
|
63
|
-
if (returning)
|
|
64
|
-
|
|
121
|
+
if (returning)
|
|
122
|
+
return reading(
|
|
123
|
+
await db.insertInto(table).values(row).returning(columns).executeTakeFirstOrThrow(),
|
|
124
|
+
)
|
|
125
|
+
if (id === undefined)
|
|
126
|
+
throw new StartError(
|
|
127
|
+
'Q002',
|
|
128
|
+
"`create` needs the id: MySQL can't return a row",
|
|
129
|
+
'generate it before the insert',
|
|
130
|
+
)
|
|
65
131
|
await db.insertInto(table).values(row).execute()
|
|
66
132
|
return (await byId(id))!
|
|
67
133
|
},
|
|
@@ -69,7 +135,12 @@ export function kyselySource<Row = Record<string, unknown>, DB = unknown>(handle
|
|
|
69
135
|
const row = written(spec, patch)
|
|
70
136
|
if (!Object.keys(row).length) return byId(id)
|
|
71
137
|
if (returning) {
|
|
72
|
-
const r = await db
|
|
138
|
+
const r = await db
|
|
139
|
+
.updateTable(table)
|
|
140
|
+
.set(row)
|
|
141
|
+
.where(idColumn, '=', id)
|
|
142
|
+
.returning(columns)
|
|
143
|
+
.executeTakeFirst()
|
|
73
144
|
return r ? reading(r) : null
|
|
74
145
|
}
|
|
75
146
|
if (!(await byId(id))) return null
|
|
@@ -77,10 +148,15 @@ export function kyselySource<Row = Record<string, unknown>, DB = unknown>(handle
|
|
|
77
148
|
return byId(id)
|
|
78
149
|
},
|
|
79
150
|
async remove(id) {
|
|
80
|
-
return (
|
|
151
|
+
return (
|
|
152
|
+
(await db.deleteFrom(table).where(idColumn, '=', id).executeTakeFirst()).numDeletedRows > 0n
|
|
153
|
+
)
|
|
81
154
|
},
|
|
82
155
|
}
|
|
83
156
|
}
|
|
84
157
|
|
|
85
158
|
/** `db.adapter@1` over a table of a Kysely instance. */
|
|
86
|
-
export const adapter: DbAdapter<Kysely<unknown>> = {
|
|
159
|
+
export const adapter: DbAdapter<Kysely<unknown>> = {
|
|
160
|
+
name: 'kysely',
|
|
161
|
+
source: (t, spec) => kyselySource(t, spec as KyselySpec),
|
|
162
|
+
}
|
package/src/mongo.ts
CHANGED
|
@@ -2,12 +2,25 @@
|
|
|
2
2
|
// checked scalar and a search is an escaped `$regex`, so a request has no way to name an operator. Takes
|
|
3
3
|
// whatever has the driver's collection methods, so `mongodb` is the app's dependency.
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
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'
|
|
6
16
|
import { hub } from '@weft-js/start/hub'
|
|
7
17
|
import type { Source } from '@weft-js/start/changes'
|
|
8
18
|
|
|
9
19
|
/** What a change stream hands over, as far as anything here reads it. */
|
|
10
|
-
export interface MongoEvent extends MongoChange {
|
|
20
|
+
export interface MongoEvent extends MongoChange {
|
|
21
|
+
ns?: { coll?: string }
|
|
22
|
+
fullDocument?: { tags?: string[] }
|
|
23
|
+
}
|
|
11
24
|
|
|
12
25
|
/** The driver's change stream. */
|
|
13
26
|
export type MongoStream = AsyncIterable<MongoEvent> & { close?(): Promise<unknown> }
|
|
@@ -24,45 +37,89 @@ export interface MongoCollection {
|
|
|
24
37
|
watch(pipeline?: object[], options?: object): MongoStream
|
|
25
38
|
}
|
|
26
39
|
|
|
27
|
-
const regex = (text: string) => ({
|
|
40
|
+
const regex = (text: string) => ({
|
|
41
|
+
$regex: text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
|
|
42
|
+
$options: 'i',
|
|
43
|
+
})
|
|
28
44
|
|
|
29
45
|
/** A `ListSource` over one collection. */
|
|
30
|
-
export function mongoSource<Row = Record<string, unknown>>(
|
|
46
|
+
export function mongoSource<Row = Record<string, unknown>>(
|
|
47
|
+
collection: MongoCollection,
|
|
48
|
+
spec: SourceSpec,
|
|
49
|
+
): ListSource<Row> {
|
|
31
50
|
const stored = Object.values(spec.columns)
|
|
32
51
|
const idColumn = storedId(spec)
|
|
33
|
-
const projection = Object.fromEntries([
|
|
52
|
+
const projection = Object.fromEntries([
|
|
53
|
+
...stored.map((c) => [c, 1]),
|
|
54
|
+
...(stored.includes('_id') ? [] : [['_id', 0]]),
|
|
55
|
+
])
|
|
34
56
|
const reading = rowOf<Row>(spec)
|
|
35
57
|
return {
|
|
36
58
|
async list(p: ListParams) {
|
|
37
59
|
const q = plan(spec, p)
|
|
38
|
-
const and: object[] = q.where.map(w => ({
|
|
39
|
-
|
|
60
|
+
const and: object[] = q.where.map((w) => ({
|
|
61
|
+
[w.column]: w.values.length === 1 ? { $eq: w.values[0] } : { $in: w.values },
|
|
62
|
+
}))
|
|
63
|
+
if (q.search) and.push({ $or: q.search.columns.map((c) => ({ [c]: regex(q.search!.text) })) })
|
|
40
64
|
const filter = and.length ? { $and: and } : {}
|
|
41
|
-
const dir = q.sort?.dir === 'desc' ? -1 : 1,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
65
|
+
const dir = q.sort?.dir === 'desc' ? -1 : 1,
|
|
66
|
+
cmp = dir < 0 ? '$lt' : '$gt'
|
|
67
|
+
const keyset =
|
|
68
|
+
p.after &&
|
|
69
|
+
(q.sort
|
|
70
|
+
? {
|
|
71
|
+
$or: [
|
|
72
|
+
{ [q.sort.column]: { [cmp]: p.after[0] } },
|
|
73
|
+
{ [q.sort.column]: { $eq: p.after[0] }, [idColumn]: { [cmp]: p.after[1] } },
|
|
74
|
+
],
|
|
75
|
+
}
|
|
76
|
+
: { [idColumn]: { $gt: p.after[1] } })
|
|
45
77
|
const sort = q.sort ? { [q.sort.column]: dir, [idColumn]: dir } : { [idColumn]: 1 }
|
|
46
78
|
const cursor = p.mode === 'cursor'
|
|
47
|
-
const rows = (
|
|
48
|
-
|
|
79
|
+
const rows = (
|
|
80
|
+
await collection
|
|
81
|
+
.find(keyset ? { $and: [...and, keyset] } : filter, {
|
|
82
|
+
projection,
|
|
83
|
+
sort,
|
|
84
|
+
skip: p.offset,
|
|
85
|
+
limit: cursor ? p.limit + 1 : p.limit,
|
|
86
|
+
})
|
|
87
|
+
.toArray()
|
|
88
|
+
).map(reading)
|
|
89
|
+
return pageOf(
|
|
90
|
+
rows,
|
|
91
|
+
p,
|
|
92
|
+
{ sort: q.sort?.field, id: q.id.field },
|
|
93
|
+
cursor ? undefined : await collection.countDocuments(filter),
|
|
94
|
+
)
|
|
95
|
+
},
|
|
96
|
+
get: async (id) => {
|
|
97
|
+
const r = await collection.findOne({ [idColumn]: { $eq: id } }, { projection })
|
|
98
|
+
return r ? reading(r) : null
|
|
49
99
|
},
|
|
50
|
-
get: async id => { const r = await collection.findOne({ [idColumn]: { $eq: id } }, { projection }); return r ? reading(r) : null },
|
|
51
100
|
async create(data) {
|
|
52
101
|
const doc = written(spec, data, true)
|
|
53
102
|
await collection.insertOne({ ...doc })
|
|
54
103
|
return reading(doc)
|
|
55
104
|
},
|
|
56
105
|
async update(id, patch) {
|
|
57
|
-
const r = await collection.findOneAndUpdate(
|
|
106
|
+
const r = await collection.findOneAndUpdate(
|
|
107
|
+
{ [idColumn]: { $eq: id } },
|
|
108
|
+
{ $set: written(spec, patch) },
|
|
109
|
+
{ projection, returnDocument: 'after' },
|
|
110
|
+
)
|
|
58
111
|
return r ? reading(r) : null
|
|
59
112
|
},
|
|
60
|
-
remove: async
|
|
113
|
+
remove: async (id) =>
|
|
114
|
+
((await collection.deleteOne({ [idColumn]: { $eq: id } })).deletedCount ?? 0) > 0,
|
|
61
115
|
}
|
|
62
116
|
}
|
|
63
117
|
|
|
64
118
|
/** `db.adapter@1` over a Mongo collection. */
|
|
65
|
-
export const adapter: DbAdapter<MongoCollection> = {
|
|
119
|
+
export const adapter: DbAdapter<MongoCollection> = {
|
|
120
|
+
name: 'mongo',
|
|
121
|
+
source: (t, spec) => mongoSource(t, spec),
|
|
122
|
+
}
|
|
66
123
|
|
|
67
124
|
/** The methods of the driver's `Db` the change feed calls. */
|
|
68
125
|
export interface MongoDb {
|
|
@@ -80,7 +137,11 @@ const said = (e: unknown) => (e as { message?: string } | null)?.message ?? Stri
|
|
|
80
137
|
* barrier is a capped collection's insert, so the oplog puts it after the writes it covers. After a lost stream every
|
|
81
138
|
* collection is poked: the resume token is not kept, because a poke is cheaper than a stale page.
|
|
82
139
|
*/
|
|
83
|
-
export function mongoChanges(
|
|
140
|
+
export function mongoChanges(
|
|
141
|
+
db: MongoDb,
|
|
142
|
+
collections: string[],
|
|
143
|
+
options: { retry?: number; external?: boolean } = {},
|
|
144
|
+
): Source {
|
|
84
145
|
const retry = options.retry ?? 1000
|
|
85
146
|
return {
|
|
86
147
|
name: 'mongodb',
|
|
@@ -90,17 +151,34 @@ export function mongoChanges(db: MongoDb, collections: string[], options: { retr
|
|
|
90
151
|
async probe() {
|
|
91
152
|
try {
|
|
92
153
|
const hello = await db.command({ hello: 1 })
|
|
93
|
-
if (hello.setName || hello.msg === 'isdbgrid')
|
|
94
|
-
|
|
154
|
+
if (hello.setName || hello.msg === 'isdbgrid')
|
|
155
|
+
return { mode: 'feed', via: 'mongo-change-stream' }
|
|
156
|
+
return {
|
|
157
|
+
mode: 'off',
|
|
158
|
+
via: 'none',
|
|
159
|
+
why: 'a standalone mongod has no oplog, so no change streams',
|
|
160
|
+
fix: 'run it as a replica set: a single member is enough (`mongod --replSet rs0`, then `rs.initiate()`)',
|
|
161
|
+
}
|
|
95
162
|
} catch (e) {
|
|
96
|
-
return {
|
|
163
|
+
return {
|
|
164
|
+
mode: 'off',
|
|
165
|
+
via: 'none',
|
|
166
|
+
why: `MongoDB didn't answer: ${said(e)}`,
|
|
167
|
+
fix: 'check the connection string',
|
|
168
|
+
}
|
|
97
169
|
}
|
|
98
170
|
},
|
|
99
171
|
async start(sink, how) {
|
|
100
172
|
if (how.mode === 'off') return () => {}
|
|
101
173
|
await db.createCollection?.(BARRIER, { capped: true, size: 1 << 20 }).catch(() => {})
|
|
102
|
-
let stream: MongoStream | undefined,
|
|
103
|
-
|
|
174
|
+
let stream: MongoStream | undefined,
|
|
175
|
+
stopped = false,
|
|
176
|
+
timer: ReturnType<typeof setTimeout> | undefined,
|
|
177
|
+
again = false
|
|
178
|
+
const pipeline = [
|
|
179
|
+
{ $match: { 'ns.coll': { $in: [...collections, BARRIER] } } },
|
|
180
|
+
{ $project: { operationType: 1, documentKey: 1, 'ns.coll': 1, 'fullDocument.tags': 1 } },
|
|
181
|
+
]
|
|
104
182
|
const run = async () => {
|
|
105
183
|
if (stopped) return
|
|
106
184
|
try {
|
|
@@ -109,9 +187,18 @@ export function mongoChanges(db: MongoDb, collections: string[], options: { retr
|
|
|
109
187
|
sink.up()
|
|
110
188
|
again = true
|
|
111
189
|
for await (const c of stream) {
|
|
112
|
-
if (c.ns?.coll === BARRIER) {
|
|
113
|
-
|
|
114
|
-
|
|
190
|
+
if (c.ns?.coll === BARRIER) {
|
|
191
|
+
if (c.fullDocument?.tags) sink.settled(c.fullDocument.tags)
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
if (!['insert', 'update', 'replace', 'delete'].includes(c.operationType) || !c.ns?.coll)
|
|
195
|
+
continue
|
|
196
|
+
sink.changes([
|
|
197
|
+
{
|
|
198
|
+
table: c.ns.coll,
|
|
199
|
+
...(c.documentKey?._id !== undefined && { id: String(c.documentKey._id) }),
|
|
200
|
+
},
|
|
201
|
+
])
|
|
115
202
|
}
|
|
116
203
|
} catch (e) {
|
|
117
204
|
if (!stopped) sink.down(`change stream: ${said(e)}`)
|
|
@@ -125,12 +212,15 @@ export function mongoChanges(db: MongoDb, collections: string[], options: { retr
|
|
|
125
212
|
await stream?.close?.()
|
|
126
213
|
}
|
|
127
214
|
},
|
|
128
|
-
settle: async tags => void (await db.collection(BARRIER).insertOne({ tags })),
|
|
215
|
+
settle: async (tags) => void (await db.collection(BARRIER).insertOne({ tags })),
|
|
129
216
|
}
|
|
130
217
|
}
|
|
131
218
|
|
|
132
219
|
/** The fields of a change-stream event that say what happened to which row. */
|
|
133
|
-
export interface MongoChange {
|
|
220
|
+
export interface MongoChange {
|
|
221
|
+
operationType: string
|
|
222
|
+
documentKey?: { _id?: string | number | { toString(): string } }
|
|
223
|
+
}
|
|
134
224
|
|
|
135
225
|
/** The tags of one change: the collection's, and the row's. Never the row itself. */
|
|
136
226
|
export function changeTags(change: MongoChange, tag: string): string[] {
|
|
@@ -153,12 +243,17 @@ export interface WatchOptions {
|
|
|
153
243
|
* Writes from cron, a shell or another service reach `.live` queries, as tags. Needs a replica set. After a lost stream
|
|
154
244
|
* the whole collection's tag is poked, because what happened meanwhile is unknown. Returns the way to stop.
|
|
155
245
|
*/
|
|
156
|
-
export function watchMongo(
|
|
246
|
+
export function watchMongo(
|
|
247
|
+
collection: MongoCollection,
|
|
248
|
+
options: WatchOptions = {},
|
|
249
|
+
): () => Promise<void> {
|
|
157
250
|
const tag = options.tag ?? collection.collectionName
|
|
158
251
|
if (!tag) throw new Error('`watchMongo` needs a `tag`')
|
|
159
252
|
const poke = options.poke ?? ((tags: string[]) => hub().poke(tags))
|
|
160
253
|
const retry = options.retry ?? 1000
|
|
161
|
-
let stream: MongoStream | undefined,
|
|
254
|
+
let stream: MongoStream | undefined,
|
|
255
|
+
stopped = false,
|
|
256
|
+
timer: ReturnType<typeof setTimeout> | undefined
|
|
162
257
|
const run = async (again: boolean) => {
|
|
163
258
|
if (stopped) return
|
|
164
259
|
try {
|
package/src/native/auth.ts
CHANGED
|
@@ -1,16 +1,40 @@
|
|
|
1
|
-
// Better Auth
|
|
2
|
-
// connection. One connection on every engine and runtime, with no second driver and no ORM in the app's code.
|
|
1
|
+
// Better Auth uses a Kysely dialect over Native's connection, without a second driver.
|
|
3
2
|
|
|
4
3
|
import type { Dialect } from 'kysely'
|
|
5
4
|
import { driver } from '../driver'
|
|
6
5
|
import type { NativeDb } from './index'
|
|
7
6
|
|
|
8
|
-
const {
|
|
7
|
+
const {
|
|
8
|
+
MysqlAdapter,
|
|
9
|
+
MysqlIntrospector,
|
|
10
|
+
MysqlQueryCompiler,
|
|
11
|
+
PostgresAdapter,
|
|
12
|
+
PostgresIntrospector,
|
|
13
|
+
PostgresQueryCompiler,
|
|
14
|
+
SqliteAdapter,
|
|
15
|
+
SqliteIntrospector,
|
|
16
|
+
SqliteQueryCompiler,
|
|
17
|
+
} = await driver('@weft-js/db/plugin-native/auth', 'kysely', () => import('kysely'))
|
|
9
18
|
|
|
10
19
|
const PARTS = {
|
|
11
|
-
sqlite: {
|
|
12
|
-
|
|
13
|
-
|
|
20
|
+
sqlite: {
|
|
21
|
+
adapter: SqliteAdapter,
|
|
22
|
+
compiler: SqliteQueryCompiler,
|
|
23
|
+
introspector: SqliteIntrospector,
|
|
24
|
+
type: 'sqlite',
|
|
25
|
+
},
|
|
26
|
+
postgresql: {
|
|
27
|
+
adapter: PostgresAdapter,
|
|
28
|
+
compiler: PostgresQueryCompiler,
|
|
29
|
+
introspector: PostgresIntrospector,
|
|
30
|
+
type: 'postgres',
|
|
31
|
+
},
|
|
32
|
+
mysql: {
|
|
33
|
+
adapter: MysqlAdapter,
|
|
34
|
+
compiler: MysqlQueryCompiler,
|
|
35
|
+
introspector: MysqlIntrospector,
|
|
36
|
+
type: 'mysql',
|
|
37
|
+
},
|
|
14
38
|
} as const
|
|
15
39
|
|
|
16
40
|
/** Better Auth's `database` option on a Native client. It runs without transactions, which Better Auth treats as optional. */
|
|
@@ -18,10 +42,13 @@ export function nativeAuth(db: NativeDb) {
|
|
|
18
42
|
const { adapter, compiler, introspector, type } = PARTS[db.$weft.dialect]
|
|
19
43
|
const connection = {
|
|
20
44
|
async executeQuery({ sql, parameters }: { sql: string; parameters: readonly unknown[] }) {
|
|
21
|
-
const out = await db.query(sql, [...parameters]) as unknown
|
|
45
|
+
const out = (await db.query(sql, [...parameters])) as unknown
|
|
22
46
|
// mysql2 answers a write with a header, not rows.
|
|
23
47
|
if (Array.isArray(out)) return { rows: out }
|
|
24
|
-
return {
|
|
48
|
+
return {
|
|
49
|
+
rows: [],
|
|
50
|
+
numAffectedRows: BigInt((out as { affectedRows?: number }).affectedRows ?? 0),
|
|
51
|
+
}
|
|
25
52
|
},
|
|
26
53
|
streamQuery(): never {
|
|
27
54
|
throw new Error('Native has no streaming queries')
|
|
@@ -38,7 +65,8 @@ export function nativeAuth(db: NativeDb) {
|
|
|
38
65
|
commitTransaction: async () => {},
|
|
39
66
|
rollbackTransaction: async () => {},
|
|
40
67
|
}),
|
|
41
|
-
createIntrospector: (kysely: ConstructorParameters<typeof SqliteIntrospector>[0]) =>
|
|
68
|
+
createIntrospector: (kysely: ConstructorParameters<typeof SqliteIntrospector>[0]) =>
|
|
69
|
+
new introspector(kysely),
|
|
42
70
|
createQueryCompiler: () => new compiler(),
|
|
43
71
|
} as unknown as Dialect
|
|
44
72
|
return { dialect, type, transaction: false }
|