@weft-js/db 0.1.1 → 0.1.3

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/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 { likeOf, pageOf, plan, rowOf, storedId, written, type DbAdapter, type ListParams, type ListSource, type SourceSpec } from '@weft-js/start/list'
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>(handle: Kysely<DB>, spec: KyselySpec): ListSource<Row> {
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
- async list(p: ListParams) {
34
- const q = plan(spec, p)
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) return reading(await db.insertInto(table).values(row).returning(columns).executeTakeFirstOrThrow())
64
- if (id === undefined) throw new StartError('Q002', '`create` needs the id: MySQL can\'t return a row', 'generate it before the insert')
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.updateTable(table).set(row).where(idColumn, '=', id).returning(columns).executeTakeFirst()
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 (await db.deleteFrom(table).where(idColumn, '=', id).executeTakeFirst()).numDeletedRows > 0n
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>> = { name: 'kysely', source: (t, spec) => kyselySource(t, spec as KyselySpec) }
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 { pageOf, plan, rowOf, storedId, written, type DbAdapter, type ListParams, type ListSource, type SourceSpec } from '@weft-js/start/list'
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 { ns?: { coll?: string }; fullDocument?: { tags?: string[] } }
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) => ({ $regex: text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), $options: 'i' })
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>>(collection: MongoCollection, spec: SourceSpec): ListSource<Row> {
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([...stored.map(c => [c, 1]), ...(stored.includes('_id') ? [] : [['_id', 0]])])
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 => ({ [w.column]: w.values.length === 1 ? { $eq: w.values[0] } : { $in: w.values } }))
39
- if (q.search) and.push({ $or: q.search.columns.map(c => ({ [c]: regex(q.search!.text) })) })
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, cmp = dir < 0 ? '$lt' : '$gt'
42
- const keyset = p.after && (q.sort
43
- ? { $or: [{ [q.sort.column]: { [cmp]: p.after[0] } }, { [q.sort.column]: { $eq: p.after[0] }, [idColumn]: { [cmp]: p.after[1] } }] }
44
- : { [idColumn]: { $gt: p.after[1] } })
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 = (await collection.find(keyset ? { $and: [...and, keyset] } : filter, { projection, sort, skip: p.offset, limit: cursor ? p.limit + 1 : p.limit }).toArray()).map(reading)
48
- return pageOf(rows, p, { sort: q.sort?.field, id: q.id.field }, cursor ? undefined : await collection.countDocuments(filter))
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({ [idColumn]: { $eq: id } }, { $set: written(spec, patch) }, { projection, returnDocument: 'after' })
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 id => ((await collection.deleteOne({ [idColumn]: { $eq: id } })).deletedCount ?? 0) > 0,
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> = { name: 'mongo', source: (t, spec) => mongoSource(t, spec) }
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(db: MongoDb, collections: string[], options: { retry?: number; external?: boolean } = {}): Source {
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') return { mode: 'feed', via: 'mongo-change-stream' }
94
- return { mode: 'off', via: 'none', why: 'a standalone mongod has no oplog, so no change streams', fix: 'run it as a replica set: a single member is enough (`mongod --replSet rs0`, then `rs.initiate()`)' }
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 { mode: 'off', via: 'none', why: `MongoDB didn't answer: ${said(e)}`, fix: 'check the connection string' }
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, stopped = false, timer: ReturnType<typeof setTimeout> | undefined, again = false
103
- const pipeline = [{ $match: { 'ns.coll': { $in: [...collections, BARRIER] } } }, { $project: { operationType: 1, documentKey: 1, 'ns.coll': 1, 'fullDocument.tags': 1 } }]
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) { if (c.fullDocument?.tags) sink.settled(c.fullDocument.tags); continue }
113
- if (!['insert', 'update', 'replace', 'delete'].includes(c.operationType) || !c.ns?.coll) continue
114
- sink.changes([{ table: c.ns.coll, ...(c.documentKey?._id !== undefined && { id: String(c.documentKey._id) }) }])
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 { operationType: string; documentKey?: { _id?: string | number | { toString(): string } } }
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(collection: MongoCollection, options: WatchOptions = {}): () => Promise<void> {
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, stopped = false, timer: ReturnType<typeof setTimeout> | 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 {
@@ -1,16 +1,40 @@
1
- // Better Auth on Native (research 34 §5): Better Auth speaks Kysely, so this is a Kysely dialect over Native's own
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 { MysqlAdapter, MysqlIntrospector, MysqlQueryCompiler, PostgresAdapter, PostgresIntrospector, PostgresQueryCompiler, SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } = await driver('@weft-js/db/plugin-native/auth', 'kysely', () => import('kysely'))
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: { adapter: SqliteAdapter, compiler: SqliteQueryCompiler, introspector: SqliteIntrospector, type: 'sqlite' },
12
- postgresql: { adapter: PostgresAdapter, compiler: PostgresQueryCompiler, introspector: PostgresIntrospector, type: 'postgres' },
13
- mysql: { adapter: MysqlAdapter, compiler: MysqlQueryCompiler, introspector: MysqlIntrospector, type: 'mysql' },
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 { rows: [], numAffectedRows: BigInt((out as { affectedRows?: number }).affectedRows ?? 0) }
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]) => new introspector(kysely),
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 }
@@ -1,5 +1,4 @@
1
- // The no-ORM client (research 34 §4): one `sql` tag over any driver. Values always go to the driver as parameters, so
2
- // a template can't build SQL from a value. An engine file supplies the driver; `createNative` takes your own the same way.
1
+ // SQL template values always go to the driver as parameters.
3
2
 
4
3
  import { gate, register, touch, type DbHandle, type Migration } from '@weft-js/start/db/core'
5
4
 
@@ -9,11 +8,9 @@ export type Row = Record<string, unknown>
9
8
  /** What an engine supplies: statements, files of them, a transaction and a lock. */
10
9
  export interface Driver<D = unknown> {
11
10
  dialect: DbHandle['dialect']
12
- /** The client underneath, for anything `sql` doesn't cover. */
13
11
  raw: D
14
12
  /** Run one statement with bound values, and return its rows as objects. */
15
13
  query(text: string, params: unknown[]): Promise<Row[]>
16
- /** Run a whole file of statements. */
17
14
  exec(text: string): Promise<void>
18
15
  /** Run `fn` on one connection, committed when it resolves. */
19
16
  transaction<T>(fn: (driver: Pick<Driver, 'query'>) => Promise<T>): Promise<T>
@@ -34,29 +31,43 @@ export interface NativeDb<D = unknown> {
34
31
  $weft: DbHandle
35
32
  }
36
33
 
37
- const placeholder = (dialect: DbHandle['dialect'], i: number) => (dialect === 'postgresql' ? `$${i + 1}` : '?')
34
+ const placeholder = (dialect: DbHandle['dialect'], i: number) =>
35
+ dialect === 'postgresql' ? `$${i + 1}` : '?'
38
36
 
39
- const tag = (handle: DbHandle, query: Driver['query']): Sql => (strings, ...values) => {
40
- const text = strings.reduce((out, part, i) => out + placeholder(handle.dialect, i - 1) + part)
41
- touch(handle, text)
42
- return query(text, values)
43
- }
37
+ const tag =
38
+ (handle: DbHandle, query: Driver['query']): Sql =>
39
+ (strings, ...values) => {
40
+ const text = strings.reduce((out, part, i) => out + placeholder(handle.dialect, i - 1) + part)
41
+ touch(handle, text)
42
+ return query(text, values)
43
+ }
44
44
 
45
45
  /** A client over any `Driver`, with its migrations behind the same gate as every statement. */
46
- export function createNative<D>(driver: Driver<D>, { migrate = true }: { migrate?: boolean | undefined } = {}): NativeDb<D> {
46
+ export function createNative<D>(
47
+ driver: Driver<D>,
48
+ { migrate = true }: { migrate?: boolean | undefined } = {},
49
+ ): NativeDb<D> {
47
50
  const handle: DbHandle = {
48
51
  dialect: driver.dialect,
49
52
  exempt: new Set(),
50
- migrate: (list: Migration[]) => driver.locked(async () => {
51
- await driver.exec('create table if not exists __weft_migrations (tag varchar(255) primary key)')
52
- const done = new Set((await driver.query('select tag from __weft_migrations', [])).map(r => r.tag))
53
- for (const m of list) {
54
- if (done.has(m.tag)) continue
55
- // A file with only comments is a migration still being written; some engines refuse an empty query.
56
- if (m.sql.replace(/--.*$/gm, '').trim()) await driver.exec(m.sql)
57
- await driver.query(`insert into __weft_migrations (tag) values (${placeholder(driver.dialect, 0)})`, [m.tag])
58
- }
59
- }),
53
+ migrate: (list: Migration[]) =>
54
+ driver.locked(async () => {
55
+ await driver.exec(
56
+ 'create table if not exists __weft_migrations (tag varchar(255) primary key)',
57
+ )
58
+ const done = new Set(
59
+ (await driver.query('select tag from __weft_migrations', [])).map((r) => r.tag),
60
+ )
61
+ for (const m of list) {
62
+ if (done.has(m.tag)) continue
63
+ // A file with only comments is a migration still being written; some engines refuse an empty query.
64
+ if (m.sql.replace(/--.*$/gm, '').trim()) await driver.exec(m.sql)
65
+ await driver.query(
66
+ `insert into __weft_migrations (tag) values (${placeholder(driver.dialect, 0)})`,
67
+ [m.tag],
68
+ )
69
+ }
70
+ }),
60
71
  }
61
72
  const ready = gate(handle, migrate)
62
73
  const query: Driver['query'] = async (text, params) => (await ready(), driver.query(text, params))
@@ -64,8 +75,10 @@ export function createNative<D>(driver: Driver<D>, { migrate = true }: { migrate
64
75
  sql: tag(handle, query),
65
76
  query,
66
77
  driver: driver.raw,
67
- transaction: async <T>(fn: (tx: { sql: Sql }) => Promise<T>) => (await ready(), driver.transaction(tx => fn({ sql: tag(handle, tx.query) }))),
78
+ transaction: async <T>(fn: (tx: { sql: Sql }) => Promise<T>) => (
79
+ await ready(),
80
+ driver.transaction((tx) => fn({ sql: tag(handle, tx.query) }))
81
+ ),
68
82
  }
69
83
  return register(db, handle)
70
84
  }
71
-
@@ -4,13 +4,25 @@
4
4
  import { driver } from '../driver'
5
5
  import { createNative, type Driver, type Row } from './index'
6
6
 
7
- const { createConnection, createPool } = await driver('@weft-js/db/plugin-native/mysql', 'mysql2', () => import('mysql2/promise'))
7
+ const { createConnection, createPool } = await driver(
8
+ '@weft-js/db/plugin-native/mysql',
9
+ 'mysql2',
10
+ () => import('mysql2/promise'),
11
+ )
8
12
 
9
13
  const LOCK = 'weft_migrations'
10
14
  type ExecuteValues = Parameters<Awaited<ReturnType<typeof createConnection>>['execute']>[1]
11
15
 
12
16
  /** A client on a mysql2 pool. */
13
- export function createDb({ url, migrate, max = 10 }: { url: string; migrate?: boolean; max?: number }) {
17
+ export function createDb({
18
+ url,
19
+ migrate,
20
+ max = 10,
21
+ }: {
22
+ url: string
23
+ migrate?: boolean
24
+ max?: number
25
+ }) {
14
26
  const uri = url || 'mysql://localhost/build'
15
27
  // No `maxIdle` below the limit: that starts mysql2's reaper, a timer that keeps a finished `weft build` alive forever.
16
28
  const pool = createPool({ uri, connectionLimit: max })
@@ -31,7 +43,9 @@ export function createDb({ url, migrate, max = 10 }: { url: string; migrate?: bo
31
43
  const c = await pool.getConnection()
32
44
  try {
33
45
  await c.beginTransaction()
34
- const out = await fn({ query: (text, params) => rows(c.execute(text, params as ExecuteValues)) })
46
+ const out = await fn({
47
+ query: (text, params) => rows(c.execute(text, params as ExecuteValues)),
48
+ })
35
49
  await c.commit()
36
50
  return out
37
51
  } catch (e) {
@@ -44,8 +58,11 @@ export function createDb({ url, migrate, max = 10 }: { url: string; migrate?: bo
44
58
  async locked(fn) {
45
59
  const c = await pool.getConnection()
46
60
  try {
47
- const [got] = await c.query('select get_lock(?, 60) as got', [LOCK]) as unknown as [{ got: unknown }[]]
48
- if (Number(got[0]?.got) !== 1) throw new Error('another instance held the migration lock for 60s')
61
+ const [got] = (await c.query('select get_lock(?, 60) as got', [LOCK])) as unknown as [
62
+ { got: unknown }[],
63
+ ]
64
+ if (Number(got[0]?.got) !== 1)
65
+ throw new Error('another instance held the migration lock for 60s')
49
66
  return await fn()
50
67
  } finally {
51
68
  await c.query('select release_lock(?)', [LOCK]).catch(() => {})