@stacksjs/database 0.58.48 → 0.58.49

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/database",
3
3
  "type": "module",
4
- "version": "0.58.48",
4
+ "version": "0.58.49",
5
5
  "description": "The Stacks database integration.",
6
6
  "author": "Chris Breuer",
7
7
  "license": "MIT",
@@ -42,7 +42,8 @@
42
42
  ],
43
43
  "files": [
44
44
  "README.md",
45
- "dist"
45
+ "dist",
46
+ "src"
46
47
  ],
47
48
  "scripts": {
48
49
  "build": "bun --bun build.ts",
package/src/index.ts ADDED
@@ -0,0 +1,63 @@
1
+ // export * from './migrations'
2
+ // export * from './seeder''
3
+ import { Kysely, MysqlDialect } from 'kysely'
4
+ import { createPool } from 'mysql2'
5
+ import type { ColumnType, Generated } from 'kysely'
6
+ import { BunWorkerDialect } from './kysely-bun-worker'
7
+
8
+ // const driver = config.database.default
9
+ const driver = 'mysql'
10
+
11
+ // const dbName = config.database.name
12
+
13
+ export interface UsersTable {
14
+ // Columns that are generated by the database should be marked
15
+ // using the `Generated` type. This way they are automatically
16
+ // made optional in inserts and updates.
17
+ id: Generated<number>
18
+
19
+ name: string
20
+ email: string
21
+
22
+ // If the column is nullable in the database, make its type nullable.
23
+ // Don't use optional properties. Optionality is always determined
24
+ // automatically by Kysely.
25
+ password: string
26
+
27
+ // You can specify a different type for each operation (select, insert and
28
+ // update) using the `ColumnType<SelectType, InsertType, UpdateType>`
29
+ // wrapper. Here we define a column `created_at` that is selected as
30
+ // a `Date`, can optionally be provided as a `string` in inserts and
31
+ // can never be updated:
32
+ created_at: ColumnType<Date, string | undefined, never>
33
+ deleted_at: ColumnType<Date, string | undefined, never>
34
+ }
35
+
36
+ export interface Database {
37
+ users: UsersTable
38
+ }
39
+
40
+ let dialect
41
+
42
+ if (driver === 'sqlite') {
43
+ dialect = new BunWorkerDialect({
44
+ url: 'stacks.sqlite',
45
+ })
46
+ }
47
+ else {
48
+ dialect = new MysqlDialect({
49
+ pool: createPool({
50
+ database: 'stacks',
51
+ host: '127.0.0.1',
52
+ user: 'root',
53
+ password: '',
54
+ port: 3306,
55
+ }),
56
+ })
57
+ }
58
+
59
+ export const db = new Kysely<Database>({
60
+ dialect,
61
+ })
62
+
63
+ export const dbDialect = dialect
@@ -0,0 +1,144 @@
1
+ /* eslint-disable eslint-comments/no-unlimited-disable */
2
+ /* eslint-disable */
3
+ import type { DatabaseConnection, Driver, QueryResult } from 'kysely'
4
+ import { CompiledQuery } from 'kysely'
5
+ import type { EmitterOnce } from './mitt'
6
+ import MittOnce from './mitt'
7
+ import type { EventWithError, MainMsg, WorkerMsg } from './type'
8
+ import type { BunWorkerDialectConfig } from '.'
9
+
10
+ export class BunWorkerDriver implements Driver {
11
+ private config?: BunWorkerDialectConfig
12
+ private worker?: Worker
13
+ private connection?: DatabaseConnection
14
+ private connectionMutex = new ConnectionMutex()
15
+ private mitt?: EmitterOnce<EventWithError>
16
+ constructor(config?: BunWorkerDialectConfig) {
17
+ this.config = config
18
+ }
19
+
20
+ async init(): Promise<void> {
21
+ this.worker = this.config?.worker ?? new Worker(
22
+ new URL('../src/kysely-bun-worker/worker', import.meta.url),
23
+ { type: 'module' },
24
+ )
25
+ this.mitt = MittOnce<EventWithError>()
26
+ this.worker.onmessage = ({ data: { data, err, type } }: MessageEvent<WorkerMsg>) => {
27
+ this.mitt?.emit(type, { data, err })
28
+ }
29
+ const msg: MainMsg = {
30
+ type: 'init',
31
+ url: this.config?.url ?? ':memory:',
32
+ cache: this.config?.cacheStatment ?? false,
33
+ }
34
+ this.worker.postMessage(msg)
35
+ await new Promise<void>((resolve, reject) => {
36
+ this.mitt?.once('init', ({ err }) => {
37
+ err ? reject(err) : resolve()
38
+ })
39
+ })
40
+ this.connection = new BunWorkerConnection(this.worker, this.mitt)
41
+
42
+ await this.config?.onCreateConnection?.(this.connection)
43
+ }
44
+
45
+ async acquireConnection(): Promise<DatabaseConnection> {
46
+ // SQLite only has one single connection. We use a mutex here to wait
47
+ // until the single connection has been released.
48
+ await this.connectionMutex.lock()
49
+ return this.connection!
50
+ }
51
+
52
+ async beginTransaction(connection: DatabaseConnection): Promise<void> {
53
+ await connection.executeQuery(CompiledQuery.raw('begin'))
54
+ }
55
+
56
+ async commitTransaction(connection: DatabaseConnection): Promise<void> {
57
+ await connection.executeQuery(CompiledQuery.raw('commit'))
58
+ }
59
+
60
+ async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
61
+ await connection.executeQuery(CompiledQuery.raw('rollback'))
62
+ }
63
+
64
+ async releaseConnection(): Promise<void> {
65
+ this.connectionMutex.unlock()
66
+ }
67
+
68
+ async destroy(): Promise<void> {
69
+ if (!this.worker)
70
+ return
71
+
72
+ this.worker.postMessage({
73
+ type: 'close',
74
+ })
75
+ return new Promise<void>((resolve, reject) => {
76
+ this.mitt?.once('close', ({ err }) => {
77
+ if (err) {
78
+ reject(err)
79
+ }
80
+ else {
81
+ this.worker?.terminate()
82
+ this.mitt?.all.clear()
83
+ this.mitt = undefined
84
+ resolve()
85
+ }
86
+ })
87
+ })
88
+ }
89
+ }
90
+
91
+ class ConnectionMutex {
92
+ private promise?: Promise<void>
93
+ private resolve?: () => void
94
+
95
+ async lock(): Promise<void> {
96
+ while (this.promise)
97
+ await this.promise
98
+
99
+ this.promise = new Promise((resolve) => {
100
+ this.resolve = resolve
101
+ })
102
+ }
103
+
104
+ unlock(): void {
105
+ const resolve = this.resolve
106
+
107
+ this.promise = undefined
108
+ this.resolve = undefined
109
+
110
+ resolve?.()
111
+ }
112
+ }
113
+
114
+ class BunWorkerConnection implements DatabaseConnection {
115
+ readonly worker: Worker
116
+ readonly mitt?: EmitterOnce<EventWithError>
117
+ constructor(worker: Worker, mitt?: EmitterOnce<EventWithError>) {
118
+ this.worker = worker
119
+ this.mitt = mitt
120
+ }
121
+
122
+ streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
123
+ throw new Error('Sqlite driver doesn\'t support streaming')
124
+ }
125
+
126
+ async executeQuery<R>(compiledQuery: CompiledQuery<unknown>): Promise<QueryResult<R>> {
127
+ const { parameters, sql, query } = compiledQuery
128
+ const mode = query.kind === 'SelectQueryNode'
129
+ ? 'query'
130
+ : query.kind === 'RawNode'
131
+ ? 'raw'
132
+ : 'exec'
133
+ const msg: MainMsg = { type: 'run', mode, sql, parameters }
134
+ this.worker.postMessage(msg)
135
+ return new Promise((resolve, reject) => {
136
+ if (!this.mitt)
137
+ reject('kysely instance has been destroyed')
138
+
139
+ this.mitt.once('run', ({ data, err }) => {
140
+ (!err && data) ? resolve(data) : reject(err)
141
+ })
142
+ })
143
+ }
144
+ }
@@ -0,0 +1,46 @@
1
+ import type { DatabaseConnection, DatabaseIntrospector, Dialect, DialectAdapter, Driver, Kysely, QueryCompiler } from 'kysely'
2
+ import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from 'kysely'
3
+ import { BunWorkerDriver } from './driver'
4
+ import type { Promisable } from './type'
5
+
6
+ export interface BunWorkerDialectConfig {
7
+ /**
8
+ * db file path
9
+ *
10
+ * @default ':memory:'
11
+ */
12
+ url?: string
13
+ onCreateConnection?: (connection: DatabaseConnection) => Promisable<void>
14
+ /**
15
+ * use bun:sqlite's built-in statment cache
16
+ * @see https://bun.sh/docs/api/sqlite#query
17
+ */
18
+ cacheStatment?: boolean
19
+ /**
20
+ * custom worker, default is a worker that use bun:sqlite
21
+ */
22
+ worker?: Worker
23
+ }
24
+
25
+ export class BunWorkerDialect implements Dialect {
26
+ #config?: BunWorkerDialectConfig
27
+ constructor(config?: BunWorkerDialectConfig) {
28
+ this.#config = config
29
+ }
30
+
31
+ createDriver(): Driver {
32
+ return new BunWorkerDriver(this.#config)
33
+ }
34
+
35
+ createQueryCompiler(): QueryCompiler {
36
+ return new SqliteQueryCompiler()
37
+ }
38
+
39
+ createAdapter(): DialectAdapter {
40
+ return new SqliteAdapter()
41
+ }
42
+
43
+ createIntrospector(db: Kysely<any>): DatabaseIntrospector {
44
+ return new SqliteIntrospector(db)
45
+ }
46
+ }
@@ -0,0 +1,24 @@
1
+ import type { Emitter, EventHandlerMap, EventType, Handler } from 'mitt'
2
+ import mitt from 'mitt'
3
+
4
+ export interface EmitterOnce<Events extends Record<EventType, unknown>> extends Emitter<Events> {
5
+ once<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void
6
+ }
7
+
8
+ export default function mittOnce<Events extends Record<EventType, unknown>>(
9
+ all?: EventHandlerMap<Events>,
10
+ ): EmitterOnce<Events> {
11
+ const emitter = mitt<Events>(all)
12
+
13
+ return {
14
+ ...emitter,
15
+
16
+ once<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>) {
17
+ const fn = (arg: Events[Key]) => {
18
+ emitter.off(type, fn)
19
+ handler(arg)
20
+ }
21
+ emitter.on(type, fn)
22
+ },
23
+ }
24
+ }
@@ -0,0 +1,40 @@
1
+ import type { QueryResult } from 'kysely'
2
+
3
+ export type Promisable<T> = T | Promise<T>
4
+
5
+ export type RunMode = 'exec' | 'query' | 'raw'
6
+
7
+ export type MainMsg =
8
+ | {
9
+ type: 'run'
10
+ mode: RunMode
11
+ sql: string
12
+ parameters?: readonly unknown[]
13
+ }
14
+ | {
15
+ type: 'close'
16
+ }
17
+ | {
18
+ type: 'init'
19
+ url: string
20
+ cache: boolean
21
+ }
22
+
23
+ export type WorkerMsg = {
24
+ [K in keyof Events]: {
25
+ type: K
26
+ data: Events[K]
27
+ err: unknown
28
+ }
29
+ }[keyof Events]
30
+ interface Events {
31
+ run: QueryResult<any> | null
32
+ init: null
33
+ close: null
34
+ }
35
+ export type EventWithError = {
36
+ [K in keyof Events]: {
37
+ data: Events[K]
38
+ err: unknown
39
+ }
40
+ }
@@ -0,0 +1,53 @@
1
+ import Database from 'bun:sqlite'
2
+ import type { QueryResult } from 'kysely'
3
+ import type { MainMsg, RunMode, WorkerMsg } from './type'
4
+
5
+ let db: Database
6
+ let cache: boolean
7
+ function run(mode: RunMode, sql: string, parameters?: readonly unknown[]): QueryResult<any> {
8
+ const stmt = db[cache ? 'query' : 'prepare'](sql)
9
+
10
+ let rows: unknown[] = []
11
+ if (mode !== 'exec')
12
+
13
+ rows = stmt.all(parameters as any)
14
+
15
+ if (mode === 'query')
16
+ return { rows }
17
+
18
+ stmt.run(parameters as any)
19
+ return {
20
+ rows,
21
+ // @ts-expect-error get insert id
22
+ insertId: db.query('SELECT last_insert_rowid() as i').get().i,
23
+ // @ts-expect-error get changes
24
+ numAffectedRows: db.query('SELECT changes() as c').get().c,
25
+ }
26
+ }
27
+
28
+ // @ts-expect-error bun worker
29
+ onmessage = ({ data }: MessageEvent<MainMsg>) => {
30
+ const ret: WorkerMsg = {
31
+ type: data.type,
32
+ data: null,
33
+ err: null,
34
+ }
35
+ try {
36
+ switch (data.type) {
37
+ case 'run':
38
+ ret.data = run(data.mode, data.sql, data.parameters)
39
+ break
40
+ case 'close':
41
+ db.close()
42
+ break
43
+ case 'init':
44
+ db = new Database(data.url, { create: true })
45
+ cache = data.cache
46
+ break
47
+ }
48
+ }
49
+ catch (error) {
50
+ ret.err = error
51
+ }
52
+ postMessage(ret)
53
+ }
@@ -0,0 +1,73 @@
1
+ // import { storage } from '@stacksjs/storage'
2
+ import { path as p } from '@stacksjs/path'
3
+ import { log } from '@stacksjs/cli'
4
+
5
+ // import type { Model, SchemaOptions } from '@stacksjs/types'
6
+ // import { titleCase } from '@stacksjs/strings'
7
+
8
+ // const { fs } = storage
9
+
10
+ // function readModelsFromFolder(folderPath: string): Promise<Model[]> {
11
+ // return new Promise((resolve, reject) => {
12
+ // const models: Model[] = []
13
+
14
+ // fs.readdir(folderPath, (err, files) => {
15
+ // if (err)
16
+ // reject(err)
17
+
18
+ // const promises = files
19
+ // .filter(file => file.endsWith('.ts'))
20
+ // .map((file) => {
21
+ // const filePath = `${folderPath}/${file}`
22
+
23
+ // return import(filePath).then((data) => {
24
+ // models.push({
25
+ // name: data.default.name,
26
+ // fields: data.default.fields,
27
+ // })
28
+ // })
29
+ // })
30
+
31
+ // Promise.all(promises)
32
+ // .then(() => resolve(models))
33
+ // .catch(err => reject(err))
34
+ // })
35
+ // })
36
+ // }
37
+
38
+ // async function migrate(path: string, options: SchemaOptions): Promise<void> {
39
+ // const models = await readModelsFromFolder(projectPath('app/Models'))
40
+
41
+ // generatePrismaSchema(models, path, options)
42
+ // }
43
+
44
+ export interface MigrationOptions {
45
+ name: string
46
+ up: string
47
+ down: string
48
+ }
49
+
50
+ export function generateMigrationFile(options: MigrationOptions) {
51
+ const { name, up, down } = options
52
+
53
+ const timestamp = new Date().getTime().toString()
54
+ const fileName = `${timestamp}-${name}.ts`
55
+ const filePath = p.frameworkPath(`database/migrations/${fileName}`)
56
+ const fileContent = `
57
+ import { Migration } from '@stacksjs/database'
58
+
59
+ export default new Migration({
60
+ name: '${name}',
61
+ up: \`
62
+ ${up}
63
+ \`,
64
+ down: \`
65
+ ${down}
66
+ \`,
67
+ })
68
+ `
69
+ // TODO: use Bun.write
70
+ fs.writeFileSync(filePath, fileContent)
71
+
72
+ log.info(`Created migration file: ${fileName}`)
73
+ }
@@ -0,0 +1,79 @@
1
+ // import { MysqlDialect, QueryBuilder, createPool } from '@stacksjs/query-builder'
2
+ // import { filesystem } from '@stacksjs/storage'
3
+ // import type { Model } from '@stacksjs/types'
4
+ // import { projectPath } from '@stacksjs/path'
5
+ // import { database as config } from '@stacksjs/config'
6
+
7
+ // const { fs } = filesystem
8
+
9
+ // function readModels(folderPath: string): Promise<Model[]> {
10
+ // return new Promise((resolve, reject) => {
11
+ // const models: Model[] = []
12
+
13
+ // fs.readdir(folderPath, (err, files) => {
14
+ // if (err)
15
+ // reject(err)
16
+
17
+ // const promises = files
18
+ // .filter(file => file.endsWith('.ts'))
19
+ // .map((file) => {
20
+ // const filePath = `${folderPath}/${file}`
21
+
22
+ // return import(filePath).then((data) => {
23
+ // models.push({
24
+ // name: data.default.name,
25
+ // fields: data.default.fields,
26
+ // useSeed: data.default.useSeed,
27
+ // })
28
+ // })
29
+ // })
30
+
31
+ // Promise.all(promises)
32
+ // .then(() => resolve(models))
33
+ // .catch(err => reject(err))
34
+ // })
35
+ // })
36
+ // }
37
+
38
+ async function seed() {
39
+ // const db = new QueryBuilder({
40
+ // dialect: new MysqlDialect({
41
+ // pool: createPool({
42
+ // database: config.database,
43
+ // host: config.host,
44
+ // password: config.password,
45
+ // user: config.username,
46
+ // }),
47
+ // }),
48
+ // })
49
+
50
+ // const models = await readModels(projectPath('app/Models'))
51
+
52
+ // const queries = models.flatMap((model) => {
53
+ // const { seedable, fields } = model
54
+
55
+ // if (!seedable)
56
+ // return []
57
+
58
+ // const count = typeof seedable === 'boolean' ? 10 : seedable.count
59
+
60
+ // const records: Record<string, any>[] = []
61
+ // for (let i = 0; i < count; i++) {
62
+ // const record: Record<string, any> = {}
63
+ // Object.entries(fields).forEach(([name, field]) => {
64
+ // if (field.factory)
65
+ // record[name] = field.factory()
66
+ // })
67
+ // records.push(record)
68
+ // }
69
+
70
+ // return model
71
+ // // return db.insertInto('users').values(records).build(sql`RETURNING *`)
72
+ // })
73
+
74
+ // const { rows } = await db.transaction().execute()
75
+
76
+ // return rows
77
+ }
78
+
79
+ export { seed }