@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/native/index.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
//
|
|
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) =>
|
|
34
|
+
const placeholder = (dialect: DbHandle['dialect'], i: number) =>
|
|
35
|
+
dialect === 'postgresql' ? `$${i + 1}` : '?'
|
|
38
36
|
|
|
39
|
-
const tag =
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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>(
|
|
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[]) =>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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>) => (
|
|
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
|
-
|
package/src/native/mysql.ts
CHANGED
|
@@ -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(
|
|
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({
|
|
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({
|
|
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 [
|
|
48
|
-
|
|
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(() => {})
|
package/src/native/postgres.ts
CHANGED
|
@@ -3,24 +3,42 @@
|
|
|
3
3
|
import { driver } from '../driver'
|
|
4
4
|
import { createNative, type Driver, type Row } from './index'
|
|
5
5
|
|
|
6
|
-
const { default: postgres } = await driver(
|
|
6
|
+
const { default: postgres } = await driver(
|
|
7
|
+
'@weft-js/db/plugin-native/postgres',
|
|
8
|
+
'postgres',
|
|
9
|
+
() => import('postgres'),
|
|
10
|
+
)
|
|
7
11
|
|
|
8
12
|
const LOCK = 7_301_987
|
|
9
13
|
|
|
10
14
|
/** A client on a postgres.js connection pool. */
|
|
11
|
-
export function createDb({
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
export function createDb({
|
|
16
|
+
url,
|
|
17
|
+
migrate,
|
|
18
|
+
max = 10,
|
|
19
|
+
}: {
|
|
20
|
+
url: string
|
|
21
|
+
migrate?: boolean
|
|
22
|
+
max?: number
|
|
23
|
+
}) {
|
|
24
|
+
const client = postgres(url || 'postgres://localhost/build', {
|
|
25
|
+
max,
|
|
26
|
+
idle_timeout: 20,
|
|
27
|
+
onnotice: () => {},
|
|
28
|
+
})
|
|
29
|
+
const on = (c: { unsafe: typeof client.unsafe }) => (text: string, params: unknown[]) =>
|
|
30
|
+
c.unsafe(text, params as Parameters<typeof client.unsafe>[1]) as unknown as Promise<Row[]>
|
|
14
31
|
const engine: Driver<typeof client> = {
|
|
15
32
|
dialect: 'postgresql',
|
|
16
33
|
raw: client,
|
|
17
34
|
query: on(client),
|
|
18
|
-
exec: async text => void (await client.unsafe(text)),
|
|
19
|
-
transaction: fn => client.begin(tx => fn({ query: on(tx) })),
|
|
20
|
-
locked: fn =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
35
|
+
exec: async (text) => void (await client.unsafe(text)),
|
|
36
|
+
transaction: (fn) => client.begin((tx) => fn({ query: on(tx) })),
|
|
37
|
+
locked: (fn) =>
|
|
38
|
+
client.begin(async (tx) => {
|
|
39
|
+
await tx.unsafe(`select pg_advisory_xact_lock(${LOCK})`)
|
|
40
|
+
return fn()
|
|
41
|
+
}),
|
|
24
42
|
}
|
|
25
43
|
return createNative(engine, { migrate })
|
|
26
44
|
}
|
package/src/native/sqlite.ts
CHANGED
|
@@ -18,12 +18,21 @@ async function open(path: string): Promise<Conn> {
|
|
|
18
18
|
if ('Bun' in globalThis) {
|
|
19
19
|
const { Database } = await import('bun:sqlite')
|
|
20
20
|
const db = new Database(path, { create: true })
|
|
21
|
-
conn = {
|
|
21
|
+
conn = {
|
|
22
|
+
all: (text, params) => db.prepare(text).all(...params),
|
|
23
|
+
exec: (text) => void db.run(text),
|
|
24
|
+
}
|
|
22
25
|
} else {
|
|
23
26
|
const { DatabaseSync } = await import('node:sqlite')
|
|
24
27
|
const db = new DatabaseSync(path)
|
|
25
28
|
// `node:sqlite` on Deno refuses a boolean, which SQLite stores as 0 or 1 anyway.
|
|
26
|
-
conn = {
|
|
29
|
+
conn = {
|
|
30
|
+
all: (text, params) =>
|
|
31
|
+
db
|
|
32
|
+
.prepare(text)
|
|
33
|
+
.all(...(params.map((v) => (typeof v === 'boolean' ? Number(v) : v)) as SQLInputValue[])),
|
|
34
|
+
exec: (text) => db.exec(text),
|
|
35
|
+
}
|
|
27
36
|
}
|
|
28
37
|
if (path !== ':memory:') conn.exec('pragma journal_mode = wal')
|
|
29
38
|
conn.exec('pragma foreign_keys = on; pragma busy_timeout = 5000')
|
|
@@ -31,7 +40,10 @@ async function open(path: string): Promise<Conn> {
|
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
/** A client on one SQLite file, or `:memory:`. */
|
|
34
|
-
export function createDb({
|
|
43
|
+
export function createDb({
|
|
44
|
+
url = 'file:.data/app.db',
|
|
45
|
+
migrate,
|
|
46
|
+
}: { url?: string; migrate?: boolean } = {}) {
|
|
35
47
|
const path = url.replace(/^file:/, '')
|
|
36
48
|
let conn: Promise<Conn> | undefined
|
|
37
49
|
const connect = () => (conn ??= open(path))
|
|
@@ -46,13 +58,15 @@ export function createDb({ url = 'file:.data/app.db', migrate }: { url?: string;
|
|
|
46
58
|
dialect: 'sqlite',
|
|
47
59
|
raw: connect,
|
|
48
60
|
query,
|
|
49
|
-
exec: async text => (await connect()).exec(text),
|
|
61
|
+
exec: async (text) => (await connect()).exec(text),
|
|
50
62
|
async transaction(fn) {
|
|
51
63
|
if (inside.getStore()) return fn({ query })
|
|
52
64
|
const c = await connect()
|
|
53
65
|
const before = gate
|
|
54
66
|
let release!: () => void
|
|
55
|
-
gate = new Promise<void>(ok => {
|
|
67
|
+
gate = new Promise<void>((ok) => {
|
|
68
|
+
release = ok
|
|
69
|
+
})
|
|
56
70
|
await before
|
|
57
71
|
try {
|
|
58
72
|
c.exec('begin')
|
|
@@ -68,7 +82,7 @@ export function createDb({ url = 'file:.data/app.db', migrate }: { url?: string;
|
|
|
68
82
|
release()
|
|
69
83
|
}
|
|
70
84
|
},
|
|
71
|
-
locked: fn => fn(),
|
|
85
|
+
locked: (fn) => fn(),
|
|
72
86
|
}
|
|
73
87
|
return createNative(driver, { migrate })
|
|
74
88
|
}
|
package/src/prisma-client.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Prisma
|
|
1
|
+
// Prisma: `withWeft(client, dialect)` returns the same client with the handle the server
|
|
2
2
|
// migrates and Q011 reads. Migrations are `prisma/migrations`, applied here because `prisma migrate deploy` isn't in a
|
|
3
3
|
// production bundle. Interactive transactions keep the lock and the statements on one connection. It stays Prisma's own
|
|
4
4
|
// client, so weft can't hold a statement: the server runs the gate per request instead, and a failed run doesn't fail it.
|
|
@@ -13,39 +13,68 @@ interface Tx {
|
|
|
13
13
|
}
|
|
14
14
|
interface Client extends Tx {
|
|
15
15
|
$on?(event: 'query', fn: (e: { query: string }) => void): void
|
|
16
|
-
$transaction<T>(
|
|
16
|
+
$transaction<T>(
|
|
17
|
+
fn: (tx: Tx) => Promise<T>,
|
|
18
|
+
options: { maxWait: number; timeout: number },
|
|
19
|
+
): Promise<T>
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
const LOCK = 7_301_988
|
|
20
|
-
const statements = (sql: string) =>
|
|
23
|
+
const statements = (sql: string) =>
|
|
24
|
+
sql
|
|
25
|
+
.split(/;[ \t]*(?:\r?\n|$)/)
|
|
26
|
+
.map((s) => s.replace(/^(\s*--.*\n)+/, '').trim())
|
|
27
|
+
.filter(Boolean)
|
|
21
28
|
|
|
22
29
|
/** The same client, with `$weft`: the handle the server migrates and Q011 reads. */
|
|
23
|
-
export function withWeft<C extends object>(
|
|
30
|
+
export function withWeft<C extends object>(
|
|
31
|
+
client: C,
|
|
32
|
+
dialect: Dialect,
|
|
33
|
+
{ migrate = true }: { migrate?: boolean | undefined } = {},
|
|
34
|
+
): C & { $weft: DbHandle } {
|
|
24
35
|
const c = client as unknown as Client
|
|
25
36
|
const handle: DbHandle = {
|
|
26
37
|
dialect,
|
|
27
38
|
exempt: new Set(),
|
|
28
|
-
migrate: (list: Migration[]) =>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
migrate: (list: Migration[]) =>
|
|
40
|
+
c.$transaction(
|
|
41
|
+
async (tx) => {
|
|
42
|
+
if (dialect === 'postgresql')
|
|
43
|
+
await tx.$queryRawUnsafe(`select pg_advisory_xact_lock(${LOCK})::text`)
|
|
44
|
+
if (dialect === 'mysql') {
|
|
45
|
+
const got = await tx.$queryRawUnsafe<{ got: unknown }[]>(
|
|
46
|
+
`select get_lock('weft_migrations', 60) as got`,
|
|
47
|
+
)
|
|
48
|
+
if (Number(got[0]?.got) !== 1)
|
|
49
|
+
throw new Error('another instance held the migration lock for 60s')
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
await tx.$executeRawUnsafe(
|
|
53
|
+
'create table if not exists __weft_migrations (tag varchar(255) primary key)',
|
|
54
|
+
)
|
|
55
|
+
const done = new Set(
|
|
56
|
+
(
|
|
57
|
+
await tx.$queryRawUnsafe<{ tag: string }[]>('select tag from __weft_migrations')
|
|
58
|
+
).map((r) => r.tag),
|
|
59
|
+
)
|
|
60
|
+
for (const m of list) {
|
|
61
|
+
if (done.has(m.tag)) continue
|
|
62
|
+
for (const s of statements(m.sql)) await tx.$executeRawUnsafe(s)
|
|
63
|
+
await tx.$executeRawUnsafe(
|
|
64
|
+
`insert into __weft_migrations (tag) values (${dialect === 'postgresql' ? '$1' : '?'})`,
|
|
65
|
+
m.tag,
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
} finally {
|
|
69
|
+
// MySQL's lock belongs to the connection, not the transaction: a failed run would keep it in the pool.
|
|
70
|
+
if (dialect === 'mysql')
|
|
71
|
+
await tx.$queryRawUnsafe(`select release_lock('weft_migrations')`).catch(() => {})
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{ maxWait: 60_000, timeout: 120_000 },
|
|
75
|
+
),
|
|
47
76
|
}
|
|
48
|
-
c.$on?.('query', e => touch(handle, e.query))
|
|
77
|
+
c.$on?.('query', (e) => touch(handle, e.query))
|
|
49
78
|
;(runtime.dbs ??= new Set()).add(gate(handle, migrate))
|
|
50
79
|
return register(client, handle)
|
|
51
80
|
}
|
package/src/prisma.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
// A list source over a Prisma model delegate: `prismaSource(ctx.db.task, { columns })`. It only reads what a delegate
|
|
2
2
|
// has, so it needs nothing from `@prisma/client` and works with any generated client.
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
pageOf,
|
|
6
|
+
plan,
|
|
7
|
+
rowOf,
|
|
8
|
+
storedId,
|
|
9
|
+
written,
|
|
10
|
+
type DbAdapter,
|
|
11
|
+
type ListParams,
|
|
12
|
+
type ListSource,
|
|
13
|
+
type SourceSpec,
|
|
14
|
+
} from '@weft-js/start/list'
|
|
5
15
|
|
|
6
16
|
/** What a generated client's model has, whatever its schema. */
|
|
7
17
|
export interface PrismaDelegate {
|
|
@@ -21,42 +31,106 @@ export interface PrismaSpec extends SourceSpec {
|
|
|
21
31
|
|
|
22
32
|
const MISSING = 'P2025'
|
|
23
33
|
|
|
34
|
+
type PrismaList<Row> = {
|
|
35
|
+
model: PrismaDelegate
|
|
36
|
+
spec: PrismaSpec
|
|
37
|
+
select: Record<string, boolean>
|
|
38
|
+
idColumn: string
|
|
39
|
+
reading: (row: object) => Row
|
|
40
|
+
escaped: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function listRows<Row>(p: ListParams, context: PrismaList<Row>) {
|
|
44
|
+
const { model, spec, select, idColumn, reading, escaped } = context
|
|
45
|
+
const q = plan(spec, p)
|
|
46
|
+
const and: object[] = q.where.map((w) => {
|
|
47
|
+
const some = w.values.filter((v) => v !== null)
|
|
48
|
+
return {
|
|
49
|
+
OR: [
|
|
50
|
+
...(some.length ? [{ [w.column]: { in: some } }] : []),
|
|
51
|
+
...(w.values.includes(null) ? [{ [w.column]: null }] : []),
|
|
52
|
+
],
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
if (q.search) {
|
|
56
|
+
const term = q.search.text.replace(/[\\%_]/g, '\\$&')
|
|
57
|
+
and.push({
|
|
58
|
+
OR: q.search.columns.map((c) => ({
|
|
59
|
+
[c]: { contains: term, ...(escaped ? { mode: 'insensitive' } : {}) },
|
|
60
|
+
})),
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
const where = { AND: and }
|
|
64
|
+
const dir = q.sort?.dir === 'desc' ? 'desc' : 'asc',
|
|
65
|
+
cmp = dir === 'desc' ? 'lt' : 'gt'
|
|
66
|
+
const keyset = p.after
|
|
67
|
+
? q.sort
|
|
68
|
+
? {
|
|
69
|
+
OR: [
|
|
70
|
+
{ [q.sort.column]: { [cmp]: p.after[0] } },
|
|
71
|
+
{ [q.sort.column]: p.after[0], [idColumn]: { [cmp]: p.after[1] } },
|
|
72
|
+
],
|
|
73
|
+
}
|
|
74
|
+
: { [idColumn]: { gt: p.after[1] } }
|
|
75
|
+
: undefined
|
|
76
|
+
const orderBy = q.sort ? [{ [q.sort.column]: dir }, { [idColumn]: dir }] : [{ [idColumn]: 'asc' }]
|
|
77
|
+
const cursor = p.mode === 'cursor'
|
|
78
|
+
const rows = (
|
|
79
|
+
await model.findMany({
|
|
80
|
+
select,
|
|
81
|
+
where: keyset ? { AND: [where, keyset] } : where,
|
|
82
|
+
orderBy,
|
|
83
|
+
take: cursor ? p.limit + 1 : p.limit,
|
|
84
|
+
skip: p.offset,
|
|
85
|
+
})
|
|
86
|
+
).map(reading)
|
|
87
|
+
return pageOf(
|
|
88
|
+
rows,
|
|
89
|
+
p,
|
|
90
|
+
{ sort: q.sort?.field, id: q.id.field },
|
|
91
|
+
cursor ? undefined : await model.count({ where }),
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
24
95
|
/** A `ListSource` over one Prisma model. */
|
|
25
|
-
export function prismaSource<Row = Record<string, unknown>>(
|
|
26
|
-
|
|
96
|
+
export function prismaSource<Row = Record<string, unknown>>(
|
|
97
|
+
model: PrismaDelegate,
|
|
98
|
+
spec: PrismaSpec,
|
|
99
|
+
): ListSource<Row> {
|
|
100
|
+
const select = Object.fromEntries(Object.values(spec.columns).map((c) => [c, true]))
|
|
27
101
|
const reading = rowOf<Row>(spec)
|
|
28
102
|
const idColumn = storedId(spec)
|
|
29
|
-
const missing = (e: unknown) => {
|
|
103
|
+
const missing = (e: unknown) => {
|
|
104
|
+
if ((e as { code?: string } | null)?.code !== MISSING) throw e
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
30
107
|
// Prisma leaves `%` and `_` as wildcards in `contains`, so a term with either is checked again here.
|
|
31
108
|
const escaped = spec.insensitive !== false
|
|
32
109
|
return {
|
|
33
110
|
limits: { search: 'cased' },
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (q.search) {
|
|
41
|
-
const term = q.search.text.replace(/[\\%_]/g, '\\$&')
|
|
42
|
-
and.push({ OR: q.search.columns.map(c => ({ [c]: { contains: term, ...(escaped ? { mode: 'insensitive' } : {}) } })) })
|
|
43
|
-
}
|
|
44
|
-
const where = { AND: and }
|
|
45
|
-
const dir = q.sort?.dir === 'desc' ? 'desc' : 'asc', cmp = dir === 'desc' ? 'lt' : 'gt'
|
|
46
|
-
const keyset = p.after
|
|
47
|
-
? q.sort ? { OR: [{ [q.sort.column]: { [cmp]: p.after[0] } }, { [q.sort.column]: p.after[0], [idColumn]: { [cmp]: p.after[1] } }] } : { [idColumn]: { gt: p.after[1] } }
|
|
48
|
-
: undefined
|
|
49
|
-
const orderBy = q.sort ? [{ [q.sort.column]: dir }, { [idColumn]: dir }] : [{ [idColumn]: 'asc' }]
|
|
50
|
-
const cursor = p.mode === 'cursor'
|
|
51
|
-
const rows = (await model.findMany({ select, where: keyset ? { AND: [where, keyset] } : where, orderBy, take: cursor ? p.limit + 1 : p.limit, skip: p.offset })).map(reading)
|
|
52
|
-
return pageOf(rows, p, { sort: q.sort?.field, id: q.id.field }, cursor ? undefined : await model.count({ where }))
|
|
111
|
+
list(p: ListParams) {
|
|
112
|
+
return listRows(p, { model, spec, select, idColumn, reading, escaped })
|
|
113
|
+
},
|
|
114
|
+
get: async (id) => {
|
|
115
|
+
const r = await model.findUnique({ where: { [idColumn]: id }, select })
|
|
116
|
+
return r ? reading(r) : null
|
|
53
117
|
},
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
update: (id, patch) => {
|
|
57
|
-
|
|
118
|
+
create: async (data) =>
|
|
119
|
+
reading(await model.create({ data: written(spec, data, true), select })),
|
|
120
|
+
update: (id, patch) => {
|
|
121
|
+
const data = written(spec, patch)
|
|
122
|
+
return model.update({ where: { [idColumn]: id }, data, select }).then(reading, missing)
|
|
123
|
+
},
|
|
124
|
+
remove: (id) =>
|
|
125
|
+
model.delete({ where: { [idColumn]: id }, select: { [idColumn]: true } }).then(
|
|
126
|
+
() => true,
|
|
127
|
+
(e) => missing(e) ?? false,
|
|
128
|
+
),
|
|
58
129
|
}
|
|
59
130
|
}
|
|
60
131
|
|
|
61
132
|
/** `db.adapter@1` over a Prisma model. */
|
|
62
|
-
export const adapter: DbAdapter<PrismaDelegate> = {
|
|
133
|
+
export const adapter: DbAdapter<PrismaDelegate> = {
|
|
134
|
+
name: 'prisma',
|
|
135
|
+
source: (t, spec) => prismaSource(t, spec),
|
|
136
|
+
}
|