@stacksjs/database 0.59.11 → 0.61.0

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/migrations.ts CHANGED
@@ -1,73 +1,172 @@
1
- import { path as p } from '@stacksjs/path'
2
- import { log } from '@stacksjs/cli'
1
+ import { dim, italic, log } from '@stacksjs/cli'
2
+ import { database } from '@stacksjs/config'
3
+ import { err, ok } from '@stacksjs/error-handling'
4
+ import { extractFieldsFromModel } from '@stacksjs/orm'
5
+ import { path } from '@stacksjs/path'
6
+ import { fs, glob } from '@stacksjs/storage'
7
+ import type { Attribute, Attributes } from '@stacksjs/types'
8
+ import { $ } from 'bun'
9
+ import { FileMigrationProvider, Migrator } from 'kysely'
10
+ import { generateMysqlMigration, resetMysqlDatabase } from './drivers'
11
+ import { generatePostgresMigration, resetPostgresDatabase } from './drivers'
12
+ import { generateSqliteMigration, resetSqliteDatabase } from './drivers'
13
+ import { db } from './utils'
3
14
 
4
- // import { storage } from '@stacksjs/storage'
5
- // import type { Model, SchemaOptions } from '@stacksjs/types'
6
- // import { titleCase } from '@stacksjs/strings'
15
+ const driver = database.default || ''
7
16
 
8
- // const { fs } = storage
17
+ export const migrator = new Migrator({
18
+ db,
9
19
 
10
- // function readModelsFromFolder(folderPath: string): Promise<Model[]> {
11
- // return new Promise((resolve, reject) => {
12
- // const models: Model[] = []
20
+ provider: new FileMigrationProvider({
21
+ fs,
22
+ path,
23
+ // This needs to be an absolute path.
24
+ migrationFolder: path.userMigrationsPath(),
25
+ }),
13
26
 
14
- // fs.readdir(folderPath, (err, files) => {
15
- // if (err)
16
- // reject(err)
27
+ migrationTableName: database.migrations,
28
+ migrationLockTableName: database.migrationLocks,
29
+ })
17
30
 
18
- // const promises = files
19
- // .filter(file => file.endsWith('.ts'))
20
- // .map((file) => {
21
- // const filePath = `${folderPath}/${file}`
31
+ // const migratorForeign = new Migrator({
32
+ // db,
22
33
 
23
- // return import(filePath).then((data) => {
24
- // models.push({
25
- // name: data.default.name,
26
- // fields: data.default.fields,
27
- // })
28
- // })
29
- // })
34
+ // provider: new FileMigrationProvider({
35
+ // fs,
36
+ // path,
37
+ // // This needs to be an absolute path.
38
+ // migrationFolder: path.userMigrationsPath('foreign'),
39
+ // }),
40
+ // })
30
41
 
31
- // Promise.all(promises)
32
- // .then(() => resolve(models))
33
- // .catch(err => reject(err))
34
- // })
35
- // })
36
- // }
42
+ export async function runDatabaseMigration() {
43
+ try {
44
+ log.info('Migrating database...')
37
45
 
38
- // async function migrate(path: string, options: SchemaOptions): Promise<void> {
39
- // const models = await readModelsFromFolder(projectPath('app/Models'))
46
+ const { error, results } = await migrator.migrateToLatest()
40
47
 
41
- // generatePrismaSchema(models, path, options)
42
- // }
48
+ if (error) {
49
+ log.error(error)
50
+ return err(error)
51
+ }
52
+
53
+ if (results?.length === 0) {
54
+ log.success('No new migrations were executed')
55
+ return ok('No new migrations were executed')
56
+ }
57
+
58
+ if (results) return ok(results)
59
+
60
+ log.success('Database migration completed with no new migrations.')
61
+ return ok('Database migration completed with no new migrations.')
62
+ } catch (error) {
63
+ console.error('Migration failed:', error)
64
+ return err(error)
65
+ }
66
+ }
43
67
 
44
68
  export interface MigrationOptions {
45
69
  name: string
46
70
  up: string
47
- down: string
48
71
  }
49
72
 
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
+ export async function resetDatabase() {
74
+ if (driver === 'sqlite') return resetSqliteDatabase()
75
+
76
+ if (driver === 'mysql') return resetMysqlDatabase()
77
+
78
+ if (driver === 'postgres') return resetPostgresDatabase()
79
+
80
+ throw new Error('Unsupported database driver in resetDatabase')
81
+ }
82
+
83
+ export async function generateMigrations() {
84
+ try {
85
+ log.info('Generating migrations...')
86
+
87
+ const modelFiles = glob.sync(path.userModelsPath('*.ts'))
88
+
89
+ for (const file of modelFiles) {
90
+ log.debug('Generating migration for:', file)
91
+ await generateMigration(file)
92
+ }
93
+
94
+ log.success('Migrations generated')
95
+ return ok('Migrations generated')
96
+ } catch (error) {
97
+ return err(error)
98
+ }
99
+ }
100
+
101
+ export async function generateMigration(modelPath: string) {
102
+ if (driver === 'sqlite') generateSqliteMigration(modelPath)
103
+
104
+ if (driver === 'mysql') generateMysqlMigration(modelPath)
105
+
106
+ if (driver === 'postgres') generatePostgresMigration(modelPath)
107
+ }
108
+
109
+ export async function getExecutedMigrations() {
110
+ try {
111
+ // @ts-expect-error the migrations table is not typed yet
112
+ return await db.selectFrom('migrations').select('name').execute()
113
+ } catch (error) {
114
+ return []
115
+ }
116
+ }
117
+
118
+ export async function haveModelFieldsChangedSinceLastMigration(modelPath: string) {
119
+ log.debug(`haveModelFieldsChangedSinceLastMigration for model: ${modelPath}`)
120
+
121
+ // const model = await import(modelPath)
122
+ // const tableName = model.default.table
123
+ // const lastMigration = await lastMigrationDate()
124
+
125
+ // now that we know the date, we need to check the git history for changes to the model file since that date
126
+ const cmd = ``
127
+ const gitHistory = await $`${cmd}`.text()
128
+
129
+ // if there are updates, then we need to check whether
130
+ // the updates include the any updates to the model
131
+ // fields that would require a migration
132
+
133
+ return !!gitHistory
134
+ }
135
+
136
+ export async function lastMigration() {
137
+ try {
138
+ // @ts-expect-error the migrations table is not typed yet
139
+ return await db.selectFrom('migrations').selectAll().orderBy('timestamp', 'desc').limit(1).execute()
140
+ } catch (error) {
141
+ console.error('Failed to get last migration:', error)
142
+ return { error }
143
+ }
144
+ }
145
+
146
+ export async function lastMigrationDate(): Promise<string | undefined> {
147
+ try {
148
+ // @ts-expect-error the migrations table is not typed yet
149
+ return (await db.selectFrom('migrations').select('timestamp').orderBy('timestamp', 'desc').limit(1).execute())[0]
150
+ .timestamp
151
+ } catch (error) {
152
+ console.error('Failed to get last migration date:', error)
153
+ return undefined
154
+ }
155
+ }
156
+
157
+ // This is a placeholder function. You need to implement the logic to
158
+ // read the last migration file and extract the fields that were modified.
159
+ export async function getLastMigrationFields(modelName: string): Promise<Attribute> {
160
+ const oldModelPath = path.frameworkPath(`database/models/${modelName}`)
161
+ const model = (await import(oldModelPath)).default as Model
162
+ let fields = {} as Attributes
163
+
164
+ if (typeof model.attributes === 'object') fields = model.attributes
165
+ else fields = JSON.parse(model.attributes) as Attributes
166
+
167
+ return fields
168
+ }
169
+
170
+ export async function getCurrentMigrationFields(modelPath: string): Promise<Attribute | undefined> {
171
+ return extractFieldsFromModel(modelPath)
73
172
  }
package/src/schema.ts CHANGED
@@ -1,11 +1,14 @@
1
+ import { log } from '@stacksjs/logging'
1
2
  import { Table } from './table'
2
3
 
3
- export class Schema {
4
- static async createTable(tableName: string, callback: (table: Table) => void): Promise<void> {
4
+ export const Schema = {
5
+ async createTable(tableName: string, callback: (table: Table) => void): Promise<void> {
5
6
  const table = new Table()
7
+
6
8
  callback(table)
9
+
7
10
  table.execute() // Simulate the execution of the table creation
8
- // eslint-disable-next-line no-console
9
- console.log(`Table "${tableName}" created.`)
10
- }
11
+
12
+ log.success(`Table "${tableName}" created.`)
13
+ },
11
14
  }
package/src/seeder.ts CHANGED
@@ -1,79 +1,169 @@
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
1
+ import { italic, log } from '@stacksjs/cli'
2
+ import { db } from '@stacksjs/database'
3
+ import { modelTableName } from '@stacksjs/orm'
4
+ import { path } from '@stacksjs/path'
5
+ import { fs, glob } from '@stacksjs/storage'
6
+ import { snakeCase } from '@stacksjs/strings'
7
+ import type { Model, RelationConfig } from '@stacksjs/types'
8
+ import { isString } from '@stacksjs/validation'
9
+ import { generateMigrations, resetDatabase, runDatabaseMigration } from './migrations'
10
+
11
+ async function seedModel(name: string, model?: Model) {
12
+ if (model?.traits?.useSeeder === false || model?.traits?.seedable === false) {
13
+ log.info(`Skipping seeding for ${italic(name)}`)
14
+ return
15
+ }
16
+
17
+ if (!model) model = (await import(path.userModelsPath(name))) as Model
18
+
19
+ const tableName = await modelTableName(model)
20
+ const seedCount =
21
+ typeof model.traits?.useSeeder === 'object' && model.traits?.useSeeder?.count ? model.traits.useSeeder.count : 10
22
+
23
+ log.info(`Seeding ${seedCount} records into ${italic(tableName)}`)
24
+
25
+ const records = []
26
+ const otherRelations = await fetchOtherModelRelations(model)
27
+
28
+ log.debug(otherRelations)
29
+
30
+ for (let i = 0; i < seedCount; i++) {
31
+ const record: any = {}
32
+
33
+ for (const fieldName in model.attributes) {
34
+ const field = model.attributes[fieldName]
35
+ // Use the factory function if available, otherwise leave the field undefined
36
+ record[fieldName] = field?.factory ? field.factory() : undefined
37
+ }
38
+
39
+ if (otherRelations?.length) {
40
+ for (let j = 0; j < otherRelations.length; j++) {
41
+ const relationElement = otherRelations[j] as RelationConfig
42
+
43
+ record[relationElement?.foreignKey] = await seedModelRelation(relationElement?.relationModel as string)
44
+ }
45
+ }
46
+
47
+ records.push(record)
48
+ }
49
+
50
+ // @ts-expect-error todo: we can improve this in the future
51
+ await db.insertInto(tableName).values(records).execute()
77
52
  }
78
53
 
79
- export { seed }
54
+ async function seedModelRelation(modelName: string): Promise<BigInt | number> {
55
+ const modelInstance = (await import(path.userModelsPath(modelName))).default
56
+
57
+ if (!modelInstance) return 1
58
+
59
+ const record: any = {}
60
+ const table = modelInstance.table
61
+
62
+ for (const fieldName in modelInstance.attributes) {
63
+ const field = modelInstance.attributes[fieldName]
64
+ // Use the factory function if available, otherwise leave the field undefined
65
+ record[fieldName] = field?.factory ? field.factory() : undefined
66
+ }
67
+
68
+ const data = await db.insertInto(table).values(record).executeTakeFirstOrThrow()
69
+
70
+ return data.insertId || 1
71
+ }
72
+
73
+ export async function getRelations(model: Model): Promise<RelationConfig[]> {
74
+ const relationsArray = ['hasOne', 'hasMany', 'belongsToMany', 'hasOneThrough']
75
+ const relationships = []
76
+
77
+ for (const relation of relationsArray) {
78
+ if (hasRelations(model, relation)) {
79
+ for (const relationInstance of model[relation]) {
80
+ let relationModel = relationInstance.model
81
+
82
+ if (isString(relationInstance)) {
83
+ relationModel = relationInstance
84
+ }
85
+
86
+ const modelRelationPath = path.userModelsPath(`${relationModel}.ts`)
87
+ const modelRelation = (await import(modelRelationPath)).default
88
+ const formattedModelName = model.name?.toLowerCase()
89
+
90
+ relationships.push({
91
+ relationship: relation,
92
+ model: relationModel,
93
+ table: modelRelation.table,
94
+ relationModel: model.name,
95
+ relationTable: model.table,
96
+ foreignKey: relationInstance.foreignKey || `${formattedModelName}_id`,
97
+ relationName: relationInstance.relationName || '',
98
+ throughModel: relationInstance.through || '',
99
+ throughForeignKey: relationInstance.throughForeignKey || '',
100
+ pivotTable: relationInstance?.pivotTable || `${formattedModelName}_${modelRelation.table}`,
101
+ })
102
+ }
103
+ }
104
+ }
105
+
106
+ return relationships
107
+ }
108
+
109
+ export async function fetchOtherModelRelations(model: Model): Promise<RelationConfig[]> {
110
+ const modelFiles = glob.sync(path.userModelsPath('*.ts'))
111
+ const modelRelations = []
112
+
113
+ for (let i = 0; i < modelFiles.length; i++) {
114
+ const modelFileElement = modelFiles[i] as string
115
+ const modelFile = await import(modelFileElement)
116
+
117
+ if (model.name === modelFile.default.name) continue
118
+
119
+ const relations = await getRelations(modelFile.default)
120
+
121
+ if (!relations.length) continue
122
+
123
+ const relation = relations.find((relation) => relation.model === model.name)
124
+
125
+ if (relation) modelRelations.push(relation)
126
+ }
127
+
128
+ return modelRelations
129
+ }
130
+
131
+ function hasRelations(obj: any, key: string): boolean {
132
+ return key in obj
133
+ }
134
+
135
+ export async function seed() {
136
+ // TODO: need to check other databases too
137
+ const dbPath = path.userDatabasePath('stacks.sqlite')
138
+
139
+ if (!fs.existsSync(dbPath)) {
140
+ log.warn('No database found, configuring it...')
141
+ // first, ensure the database is reset
142
+ await resetDatabase()
143
+
144
+ // then, generate the migrations
145
+ await generateMigrations()
146
+
147
+ // finally, migrate the database
148
+ await runDatabaseMigration()
149
+ } else {
150
+ log.debug('Database configured...')
151
+ }
152
+
153
+ // if a custom seeder exists, use it instead
154
+ const customSeederPath = path.userDatabasePath('seeder.ts')
155
+ if (fs.existsSync(customSeederPath)) {
156
+ log.info('Custom seeder found')
157
+ await import(customSeederPath)
158
+ }
159
+
160
+ // otherwise, seed all models
161
+ const modelsDir = path.userModelsPath()
162
+ const modelFiles = fs.readdirSync(modelsDir).filter((file) => file.endsWith('.ts'))
163
+
164
+ for (const file of modelFiles) {
165
+ const modelPath = path.join(modelsDir, file)
166
+ const model = await import(modelPath)
167
+ await seedModel(file, model.default)
168
+ }
169
+ }
package/src/table.ts CHANGED
@@ -1,15 +1,19 @@
1
+ import { log } from '@stacksjs/logging'
1
2
  import { Column } from './column'
2
3
 
3
4
  export class Table {
4
5
  private columns: Column[] = []
5
6
 
6
7
  increments(name: string): Column {
7
- const column = new Column(name, 'integer', { primaryKey: true, autoIncrement: true })
8
+ const column = new Column(name, 'integer', {
9
+ primaryKey: true,
10
+ autoIncrement: true,
11
+ })
8
12
  this.columns.push(column)
9
13
  return column
10
14
  }
11
15
 
12
- string(name: string, varchar: number = 255): Column {
16
+ string(name: string, varchar = 255): Column {
13
17
  const column = new Column(name, `varchar(${varchar})`)
14
18
  this.columns.push(column)
15
19
  return column
@@ -22,8 +26,7 @@ export class Table {
22
26
 
23
27
  // Method to simulate the execution of the schema definition
24
28
  execute(): void {
25
- // eslint-disable-next-line no-console
26
- console.log(`Creating table with columns: ${this.columns.map(col => col.name).join(', ')}`)
27
- // Here you would normally execute the SQL commands to create the table and columns in the database
29
+ log.info(`Creating table with columns: ${this.columns.map((col) => col.name).join(', ')}`)
30
+ // run kysely mirgration
28
31
  }
29
32
  }
package/src/types.ts CHANGED
@@ -1 +1,3 @@
1
- export { Kysely as Migration } from 'kysely'
1
+ export { Kysely as Database } from 'kysely'
2
+
3
+ export { sql } from 'kysely'
package/src/utils.ts CHANGED
@@ -1,3 +1,52 @@
1
- import { sql } from 'kysely'
1
+ import { database } from '@stacksjs/config'
2
+ import { log } from '@stacksjs/logging'
3
+ import type { Database } from '@stacksjs/orm'
4
+ import { Kysely, MysqlDialect, PostgresDialect, sql } from 'kysely'
5
+ import { BunWorkerDialect } from 'kysely-bun-worker'
6
+ import { createPool } from 'mysql2'
7
+ import { Pool } from 'pg'
8
+
9
+ export function getDialect() {
10
+ const driver = database.default ?? 'sqlite'
11
+
12
+ log.debug(`Using database driver: ${driver}`)
13
+
14
+ if (driver === 'sqlite') {
15
+ const path = database.connections?.sqlite.database ?? 'database/stacks.sqlite'
16
+ return new BunWorkerDialect({
17
+ url: path,
18
+ })
19
+ }
20
+
21
+ if (driver === 'mysql') {
22
+ return new MysqlDialect({
23
+ pool: createPool({
24
+ database: database.connections?.mysql?.name ?? 'stacks',
25
+ host: database.connections?.mysql?.host ?? '127.0.0.1',
26
+ user: database.connections?.mysql?.username ?? 'root',
27
+ password: database.connections?.mysql?.password ?? '',
28
+ port: database.connections?.mysql?.port ?? 3306,
29
+ }),
30
+ })
31
+ }
32
+
33
+ if (driver === 'postgres') {
34
+ return new PostgresDialect({
35
+ pool: new Pool({
36
+ database: database.connections?.postgres?.name ?? 'stacks',
37
+ host: database.connections?.postgres?.host ?? '127.0.0.1',
38
+ user: database.connections?.postgres?.username ?? '',
39
+ password: database.connections?.postgres?.password ?? '',
40
+ port: database.connections?.postgres?.port ?? 5432,
41
+ }),
42
+ })
43
+ }
44
+
45
+ throw new Error(`Unsupported driver: ${driver}`)
46
+ }
2
47
 
3
48
  export const now = sql`now()`
49
+
50
+ export const db = new Kysely<Database>({
51
+ dialect: getDialect(),
52
+ })
package/dist/column.d.ts DELETED
@@ -1,18 +0,0 @@
1
- interface Options {
2
- notNull?: boolean;
3
- default?: any;
4
- primaryKey?: boolean;
5
- autoIncrement?: boolean;
6
- }
7
- type ColumnType = 'integer' | 'varchar' | 'timestamp';
8
- export declare class Column {
9
- name: string;
10
- type: ColumnType;
11
- options: Options;
12
- constructor(name: string, type: ColumnType, options?: Options);
13
- notNullable(): this;
14
- defaultTo(value: any): this;
15
- primary(): this;
16
- autoIncrement(): this;
17
- }
18
- export {};
package/dist/index.d.ts DELETED
@@ -1,19 +0,0 @@
1
- import { Kysely } from 'kysely';
2
- import type { ColumnType, Generated } from 'kysely';
3
- export * from './schema';
4
- export * from './migrations';
5
- export * from './types';
6
- export * from './utils';
7
- export interface UsersTable {
8
- id: Generated<number>;
9
- name: string;
10
- email: string;
11
- password: string;
12
- created_at: ColumnType<Date, string | undefined, never>;
13
- deleted_at: ColumnType<Date, string | undefined, never>;
14
- }
15
- export interface Database {
16
- users: UsersTable;
17
- }
18
- export declare const db: Kysely<Database>;
19
- export declare const dbDialect: any;
@@ -1,6 +0,0 @@
1
- export interface MigrationOptions {
2
- name: string;
3
- up: string;
4
- down: string;
5
- }
6
- export declare function generateMigrationFile(options: MigrationOptions): void;
package/dist/schema.d.ts DELETED
@@ -1,4 +0,0 @@
1
- import { Table } from './table';
2
- export declare class Schema {
3
- static createTable(tableName: string, callback: (table: Table) => void): Promise<void>;
4
- }
package/dist/table.d.ts DELETED
@@ -1,8 +0,0 @@
1
- import { Column } from './column';
2
- export declare class Table {
3
- private columns;
4
- increments(name: string): Column;
5
- string(name: string, varchar?: number): Column;
6
- timestamps(): void;
7
- execute(): void;
8
- }
package/dist/types.d.ts DELETED
@@ -1 +0,0 @@
1
- export { Kysely as Migration } from 'kysely';
package/dist/utils.d.ts DELETED
@@ -1 +0,0 @@
1
- export declare const now: import("kysely").RawBuilder<unknown>;