velocious 1.0.14 → 1.0.16

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.
Files changed (44) hide show
  1. package/package.json +6 -2
  2. package/peak_flow.yml +12 -4
  3. package/spec/cli/commands/db/create-spec.js +21 -11
  4. package/spec/cli/commands/db/migrate-spec.js +21 -9
  5. package/spec/database/drivers/mysql/connection-spec.js +9 -7
  6. package/spec/database/record/create-spec.js +1 -1
  7. package/spec/database/record/destroy-spec.js +15 -0
  8. package/spec/database/record/translation-fallbacks-spec.js +1 -1
  9. package/spec/dummy/db/.keep +0 -0
  10. package/spec/dummy/index.js +20 -5
  11. package/spec/dummy/src/config/configuration.peakflow.sqlite.js +37 -0
  12. package/spec/dummy/src/config/configuration.sqlite.js +37 -0
  13. package/spec/dummy/src/config/routes.js +2 -0
  14. package/spec/dummy/src/database/migrations/20230728075328-create-projects.js +0 -1
  15. package/spec/dummy/src/database/migrations/20230728075329-create-tasks.js +1 -1
  16. package/spec/dummy/src/database/migrations/20250605133926-create-project-translations.js +1 -1
  17. package/spec/dummy/src/routes/projects/controller.js +28 -0
  18. package/spec/dummy/src/routes/tasks/controller.js +2 -1
  19. package/spec/http-server/post-spec.js +15 -15
  20. package/src/cli/commands/db/create.js +5 -3
  21. package/src/cli/index.js +2 -0
  22. package/src/configuration.js +10 -0
  23. package/src/database/drivers/base.js +5 -2
  24. package/src/database/drivers/mysql/foreign-key.js +13 -0
  25. package/src/database/drivers/mysql/index.js +13 -0
  26. package/src/database/drivers/mysql/sql/create-index.js +4 -0
  27. package/src/database/drivers/mysql/table.js +22 -0
  28. package/src/database/drivers/sqlite/base.js +4 -0
  29. package/src/database/drivers/sqlite/foreign-key.js +14 -0
  30. package/src/database/drivers/sqlite/index.js +1 -2
  31. package/src/database/drivers/sqlite/sql/create-index.js +1 -1
  32. package/src/database/drivers/sqlite/table.js +15 -1
  33. package/src/database/initializer-from-require-context.js +5 -1
  34. package/src/database/migration/index.js +40 -5
  35. package/src/database/pool/async-tracked-multi-connection.js +0 -12
  36. package/src/database/pool/base.js +22 -1
  37. package/src/database/pool/single-multi-use.js +0 -12
  38. package/src/database/query/create-index-base.js +1 -1
  39. package/src/database/query/create-table-base.js +29 -8
  40. package/src/database/query/index.js +8 -0
  41. package/src/database/query/insert-base.js +23 -8
  42. package/src/database/record/index.js +4 -0
  43. package/src/database/table-data/index.js +20 -0
  44. /package/spec/dummy/src/config/{configuration.peakflow.js → configuration.peakflow.mariadb.js} +0 -0
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "velocious": "bin/velocious.js"
4
4
  },
5
5
  "name": "velocious",
6
- "version": "1.0.14",
6
+ "version": "1.0.16",
7
7
  "main": "index.js",
8
8
  "scripts": {
9
9
  "test": "jasmine",
@@ -25,16 +25,20 @@
25
25
  "jasmine": "^5.0.2",
26
26
  "mysql": "^2.18.1",
27
27
  "node-fetch": "^3.3.1",
28
- "require-context": "^1.1.0"
28
+ "require-context": "^1.1.0",
29
+ "sqlite": "^5.1.1",
30
+ "sqlite3": "^5.1.7"
29
31
  },
30
32
  "dependencies": {
31
33
  "better-localstorage": "^1.0.7",
34
+ "debounce": "^2.2.0",
32
35
  "diggerize": "^1.0.5",
33
36
  "ejs": "^3.1.6",
34
37
  "escape-string-regexp": "^1.0.5",
35
38
  "incorporator": "^1.0.2",
36
39
  "inflection": "^3.0.0",
37
40
  "sql-escape-string": "^1.1.0",
41
+ "sql-string-escape": "^1.1.6",
38
42
  "sql.js": "^1.12.0",
39
43
  "strftime": "^0.10.2"
40
44
  }
package/peak_flow.yml CHANGED
@@ -5,9 +5,7 @@ before_install:
5
5
  - sudo apt-get update
6
6
  - sudo apt-get install -y nodejs
7
7
  before_script:
8
- - cp spec/dummy/src/config/configuration.peakflow.js spec/dummy/src/config/configuration.js
9
8
  - npm install
10
- - wait-for-it mariadb:3306
11
9
  services:
12
10
  mariadb:
13
11
  environment:
@@ -20,5 +18,15 @@ services:
20
18
  - 3306
21
19
  mem_limit: 4096m
22
20
  restart_policy: on-failure
23
- script:
24
- - npm test
21
+ builds:
22
+ build_1:
23
+ name: MariaDB
24
+ script:
25
+ - cp spec/dummy/src/config/configuration.peakflow.mariadb.js spec/dummy/src/config/configuration.js
26
+ - wait-for-it mariadb:3306
27
+ - npm test
28
+ build_2:
29
+ name: SQLite
30
+ script:
31
+ - cp spec/dummy/src/config/configuration.peakflow.sqlite.js spec/dummy/src/config/configuration.js
32
+ - npm test
@@ -10,16 +10,26 @@ describe("Cli - Commands - db:create", () => {
10
10
  })
11
11
  const result = await cli.execute()
12
12
 
13
- expect(result).toEqual(
14
- [
15
- {
16
- databaseName: 'velocious_test',
17
- sql: 'CREATE DATABASE IF NOT EXISTS `velocious_test`'
18
- },
19
- {
20
- createSchemaMigrationsTableSql: 'CREATE TABLE IF NOT EXISTS schema_migrations (`version` varchar(255) PRIMARY KEY NOT NULL)'
21
- }
22
- ]
23
- )
13
+ if (cli.getConfiguration().getDatabaseType() == "sqlite") {
14
+ expect(result).toEqual(
15
+ [
16
+ {
17
+ createSchemaMigrationsTableSql: 'CREATE TABLE IF NOT EXISTS schema_migrations (`version` VARCHAR(255) PRIMARY KEY NOT NULL)'
18
+ }
19
+ ]
20
+ )
21
+ } else {
22
+ expect(result).toEqual(
23
+ [
24
+ {
25
+ databaseName: 'velocious_test',
26
+ sql: 'CREATE DATABASE IF NOT EXISTS `velocious_test`'
27
+ },
28
+ {
29
+ createSchemaMigrationsTableSql: 'CREATE TABLE IF NOT EXISTS schema_migrations (`version` VARCHAR(255) PRIMARY KEY NOT NULL)'
30
+ }
31
+ ]
32
+ )
33
+ }
24
34
  })
25
35
  })
@@ -12,28 +12,40 @@ describe("Cli - Commands - db:migrate", () => {
12
12
 
13
13
  await cli.loadConfiguration()
14
14
 
15
- const db = cli.configuration.getDatabasePool()
15
+ const dbPool = cli.configuration.getDatabasePool()
16
16
 
17
- await db.withConnection(async () => {
17
+ await dbPool.withConnection(async (db) => {
18
18
  await db.query("DROP TABLE IF EXISTS tasks")
19
- await db.query("DROP TABLE IF EXISTS projects")
20
19
  await db.query("DROP TABLE IF EXISTS project_translations")
20
+ await db.query("DROP TABLE IF EXISTS projects")
21
21
  })
22
22
 
23
23
  await cli.execute()
24
24
 
25
- let tablesResult
25
+ let projectForeignKey, tablesResult
26
+
27
+ await dbPool.withConnection(async (db) => {
28
+ const tables = await db.getTables()
26
29
 
27
- await db.withConnection(async () => {
28
- tablesResult = await db.query("SHOW TABLES")
30
+ tablesResult = tables.map((table) => table.getName())
31
+
32
+ const table = await db.getTableByName("tasks")
33
+ const foreignKeys = await table.getForeignKeys()
34
+
35
+ projectForeignKey = foreignKeys.find((foreignKey) => foreignKey.getColumnName() == "project_id")
29
36
  })
30
37
 
31
38
  expect(tablesResult).toEqual(
32
39
  [
33
- {Tables_in_velocious_test: "project_translations"},
34
- {Tables_in_velocious_test: "projects"},
35
- {Tables_in_velocious_test: "tasks"}
40
+ "project_translations",
41
+ "projects",
42
+ "tasks"
36
43
  ]
37
44
  )
45
+
46
+ expect(projectForeignKey.getTableName()).toEqual("tasks")
47
+ expect(projectForeignKey.getColumnName()).toEqual("project_id")
48
+ expect(projectForeignKey.getReferencedTableName()).toEqual("projects")
49
+ expect(projectForeignKey.getReferencedColumnName()).toEqual("id")
38
50
  })
39
51
  })
@@ -6,15 +6,17 @@ const mysqlConfig = digg(configuration, "database", "default", "master")
6
6
 
7
7
  describe("Database - Drivers - Mysql - Connection", () => {
8
8
  it("connects", async () => {
9
- const mysql = new DatabaseDriversMysql(mysqlConfig)
9
+ if (configuration.getDatabaseType() != "sqlite") {
10
+ const mysql = new DatabaseDriversMysql(mysqlConfig)
10
11
 
11
- await mysql.connect()
12
+ await mysql.connect()
12
13
 
13
- const result = await mysql.query("SELECT \"1\" AS test1, \"2\" AS test2")
14
+ const result = await mysql.query("SELECT \"1\" AS test1, \"2\" AS test2")
14
15
 
15
- expect(result).toEqual([{
16
- test1: "1",
17
- test2: "2"
18
- }])
16
+ expect(result).toEqual([{
17
+ test1: "1",
18
+ test2: "2"
19
+ }])
20
+ }
19
21
  })
20
22
  })
@@ -2,7 +2,7 @@ import Dummy from "../../dummy/index.js"
2
2
  import Task from "../../dummy/src/models/task.js"
3
3
 
4
4
  describe("Record - create", () => {
5
- it("creates a new simple record", async () => {
5
+ it("creates a new simple record with relationships and translations", async () => {
6
6
  await Dummy.run(async () => {
7
7
  const task = new Task({name: "Test task"})
8
8
  const project = task.buildProject({nameEn: "Test project", nameDe: "Test projekt"})
@@ -14,4 +14,19 @@ describe("Record - destroy", () => {
14
14
  expect(foundTask).toEqual(undefined)
15
15
  })
16
16
  })
17
+
18
+ it("destroys all records in a collection", async () => {
19
+ await Dummy.run(async () => {
20
+ const task1 = await Task.create({name: "Test task 1"})
21
+ const task2 = await Task.create({name: "Test task 2"})
22
+
23
+ await Task.where({id: task1.id()}).destroyAll()
24
+
25
+ const foundTask1 = await Task.where({id: task1.id()}).first()
26
+ const foundTask2 = await Task.where({id: task2.id()}).first()
27
+
28
+ expect(foundTask1).toEqual(undefined)
29
+ expect(foundTask2).toBeDefined()
30
+ })
31
+ })
17
32
  })
@@ -2,7 +2,7 @@ import Dummy from "../../dummy/index.js"
2
2
  import Task from "../../dummy/src/models/task.js"
3
3
 
4
4
  describe("Record - create", () => {
5
- it("creates a new simple record", async () => {
5
+ it("creates a new simple record with relationships and translations with fallbacks", async () => {
6
6
  await Dummy.run(async () => {
7
7
  const task = new Task({name: "Test task"})
8
8
  const project = task.buildProject({nameDe: "Test projekt"})
File without changes
@@ -1,5 +1,6 @@
1
1
  import Application from "../../src/application.js"
2
2
  import dummyConfiguration from "./src/config/configuration.js"
3
+ import Migration from "../../src/database/migration/index.js"
3
4
 
4
5
  export default class Dummy {
5
6
  static current() {
@@ -17,13 +18,27 @@ export default class Dummy {
17
18
 
18
19
  await db.withConnection(async () => {
19
20
  await db.query("DROP TABLE IF EXISTS tasks")
20
- await db.query("DROP TABLE IF EXISTS projects")
21
21
  await db.query("DROP TABLE IF EXISTS project_translations")
22
+ await db.query("DROP TABLE IF EXISTS projects")
23
+
24
+ const migration = new Migration({configuration: dummyConfiguration})
25
+
26
+ await migration.createTable("projects", (t) => {
27
+ t.timestamps()
28
+ })
29
+ await migration.createTable("project_translations", (t) => {
30
+ t.references("project", {null: false, foreignKey: true})
31
+ t.string("locale", {null: false})
32
+ t.string("name")
33
+ })
34
+
35
+ await migration.addIndex("project_translations", ["project_id", "locale"], {unique: true})
22
36
 
23
- await db.query("CREATE TABLE tasks (id MEDIUMINT NOT NULL AUTO_INCREMENT, project_id MEDIUMINT, name VARCHAR(255), description TEXT, PRIMARY KEY (id))")
24
- await db.query("CREATE TABLE projects (id MEDIUMINT NOT NULL AUTO_INCREMENT, PRIMARY KEY (id))")
25
- await db.query("CREATE TABLE project_translations (id MEDIUMINT NOT NULL AUTO_INCREMENT, project_id MEDIUMINT, locale VARCHAR(255), name VARCHAR(255), PRIMARY KEY (id))")
26
- await db.query("CREATE UNIQUE INDEX unique_project_translation ON project_translations (project_id, locale)")
37
+ await migration.createTable("tasks", (t) => {
38
+ t.references("project")
39
+ t.string("name")
40
+ t.text("description")
41
+ })
27
42
 
28
43
  if (!dummyConfiguration.isInitialized()) {
29
44
  await dummyConfiguration.initialize()
@@ -0,0 +1,37 @@
1
+ import Configuration from "../../../../src/configuration.js"
2
+ import dummyDirectory from "../../dummy-directory.js"
3
+ import fs from "fs/promises"
4
+ import InitializerFromRequireContext from "../../../../src/database/initializer-from-require-context.js"
5
+ import SqliteDriver from "../../../../src/database/drivers/sqlite/index.js"
6
+ import path from "path"
7
+ import requireContext from "require-context"
8
+ import SingleMultiUsePool from "../../../../src/database/pool/single-multi-use.js"
9
+
10
+ export default new Configuration({
11
+ database: {
12
+ default: {
13
+ master: {
14
+ driver: SqliteDriver,
15
+ poolType: SingleMultiUsePool,
16
+ type: "sqlite",
17
+ name: "test-db"
18
+ }
19
+ }
20
+ },
21
+ directory: dummyDirectory(),
22
+ initializeModels: async ({configuration}) => {
23
+ const modelsPath = await fs.realpath(`${path.dirname(import.meta.dirname)}/../src/models`)
24
+ const requireContextModels = requireContext(modelsPath, true, /^(.+)\.js$/)
25
+ const initializerFromRequireContext = new InitializerFromRequireContext({requireContext: requireContextModels})
26
+
27
+ await configuration.getDatabasePool().withConnection(async () => {
28
+ await initializerFromRequireContext.initialize({configuration})
29
+ })
30
+ },
31
+ locale: () => "en",
32
+ localeFallbacks: {
33
+ de: ["de", "en"],
34
+ en: ["en", "de"]
35
+ },
36
+ locales: ["de", "en"]
37
+ })
@@ -0,0 +1,37 @@
1
+ import Configuration from "../../../../src/configuration.js"
2
+ import dummyDirectory from "../../dummy-directory.js"
3
+ import fs from "fs/promises"
4
+ import InitializerFromRequireContext from "../../../../src/database/initializer-from-require-context.js"
5
+ import path from "path"
6
+ import requireContext from "require-context"
7
+ import SqliteDriver from "../../../../src/database/drivers/sqlite/index.js"
8
+ import SingleMultiUsePool from "../../../../src/database/pool/single-multi-use.js"
9
+
10
+ export default new Configuration({
11
+ database: {
12
+ default: {
13
+ master: {
14
+ driver: SqliteDriver,
15
+ poolType: SingleMultiUsePool,
16
+ type: "sqlite",
17
+ name: "test-db"
18
+ }
19
+ }
20
+ },
21
+ directory: dummyDirectory(),
22
+ initializeModels: async ({configuration}) => {
23
+ const modelsPath = await fs.realpath(`${path.dirname(import.meta.dirname)}/../src/models`)
24
+ const requireContextModels = requireContext(modelsPath, true, /^(.+)\.js$/)
25
+ const initializerFromRequireContext = new InitializerFromRequireContext({requireContext: requireContextModels})
26
+
27
+ await configuration.getDatabasePool().withConnection(async () => {
28
+ await initializerFromRequireContext.initialize({configuration})
29
+ })
30
+ },
31
+ locale: () => "en",
32
+ localeFallbacks: {
33
+ de: ["de", "en"],
34
+ en: ["en", "de"]
35
+ },
36
+ locales: ["de", "en"]
37
+ })
@@ -3,6 +3,8 @@ import Routes from "../../../../src/routes/index.js"
3
3
  const routes = new Routes()
4
4
 
5
5
  routes.draw((route) => {
6
+ route.resources("projects")
7
+
6
8
  route.resources("tasks", (route) => {
7
9
  route.get("users")
8
10
  })
@@ -3,7 +3,6 @@ import Migration from "../../../../../src/database/migration/index.js"
3
3
  export default class CreateProjects extends Migration {
4
4
  async change() {
5
5
  await this.createTable("projects", (table) => {
6
- table.string("name", {maxLength: 100, null: false})
7
6
  table.timestamps()
8
7
  })
9
8
  }
@@ -3,7 +3,7 @@ import Migration from "../../../../../src/database/migration/index.js"
3
3
  export default class CreateTasks extends Migration {
4
4
  async change() {
5
5
  await this.createTable("tasks", (table) => {
6
- table.references("project")
6
+ table.references("project", {foreignKey: true, null: false})
7
7
  table.string("name")
8
8
  table.text("description")
9
9
  table.timestamps()
@@ -3,7 +3,7 @@ import Migration from "../../../../../src/database/migration/index.js"
3
3
  export default class CreateProjectTranslations extends Migration {
4
4
  async up() {
5
5
  await this.createTable("project_translations", (t) => {
6
- t.references("project", {null: false})
6
+ t.references("project", {foreignKey: true, null: false})
7
7
  t.string("locale", {null: false})
8
8
  t.string("name")
9
9
  t.timestamps()
@@ -0,0 +1,28 @@
1
+ import {digg} from "diggerize"
2
+
3
+ import Controller from "../../../../../src/controller.js"
4
+ import Project from "../../models/project.js"
5
+ import Task from "../../models/task.js"
6
+
7
+ export default class ProjectsController extends Controller {
8
+ index() {
9
+ this.viewParams.numbers = [1, 2, 3, 4, 5]
10
+ this.render()
11
+ }
12
+
13
+ async show() {
14
+ const projectId = digg(params, "id")
15
+ const project = await Project.find(projectId)
16
+
17
+ this.viewParams.project = project
18
+ this.render()
19
+ }
20
+
21
+ async create() {
22
+ const project = new Project(this.params().project)
23
+
24
+ await project.save()
25
+
26
+ this.render({json: {status: "success"}})
27
+ }
28
+ }
@@ -1,5 +1,6 @@
1
- import Controller from "../../../../../src/controller.js"
2
1
  import {digg} from "diggerize"
2
+
3
+ import Controller from "../../../../../src/controller.js"
3
4
  import Task from "../../models/task.js"
4
5
 
5
6
  export default class TasksController extends Controller {
@@ -1,14 +1,15 @@
1
1
  import fetch from "node-fetch"
2
- import Dummy from "../dummy/index.js"
3
2
  import querystring from "querystring"
4
- import Task from "../dummy/src/models/task.js"
3
+
4
+ import Dummy from "../dummy/index.js"
5
+ import Project from "../dummy/src/models/project.js"
5
6
 
6
7
  describe("HttpServer", () => {
7
8
  it("handles post requests", async () => {
8
9
  await Dummy.run(async () => {
9
- const postData = querystring.stringify({"task[name]": "Test create task"})
10
+ const postData = querystring.stringify({"project[name]": "Test create project"})
10
11
  const response = await fetch(
11
- "http://localhost:3006/tasks",
12
+ "http://localhost:3006/projects",
12
13
  {
13
14
  body: postData,
14
15
  headers: {
@@ -19,18 +20,18 @@ describe("HttpServer", () => {
19
20
  }
20
21
  )
21
22
  const text = await response.text()
22
- const createdTask = await Task.last()
23
+ const createdProject = await Project.preload({translations: true}).last()
23
24
 
24
25
  expect(text).toEqual('{"status":"success"}')
25
- expect(createdTask.readAttribute("name")).toEqual("Test create task")
26
+ expect(createdProject.name()).toEqual("Test create project")
26
27
  })
27
28
  })
28
29
 
29
30
  it("handles post json requests", async () => {
30
31
  await Dummy.run(async () => {
31
- const postData = JSON.stringify({task: {name: "Test create task"}})
32
+ const postData = JSON.stringify({project: {name: "Test create project"}})
32
33
  const response = await fetch(
33
- "http://localhost:3006/tasks",
34
+ "http://localhost:3006/projects",
34
35
  {
35
36
  body: postData,
36
37
  headers: {
@@ -41,10 +42,10 @@ describe("HttpServer", () => {
41
42
  }
42
43
  )
43
44
  const text = await response.text()
44
- const createdTask = await Task.last()
45
+ const createdProject = await Project.preload({translations: true}).last()
45
46
 
46
47
  expect(text).toEqual('{"status":"success"}')
47
- expect(createdTask.readAttribute("name")).toEqual("Test create task")
48
+ expect(createdProject.name()).toEqual("Test create project")
48
49
  })
49
50
  })
50
51
 
@@ -52,21 +53,20 @@ describe("HttpServer", () => {
52
53
  await Dummy.run(async () => {
53
54
  const body = new FormData()
54
55
 
55
- body.append("task[name]", "Test create task")
56
- body.append("task[description]", "This is a task")
56
+ body.append("project[name]", "Test create project")
57
57
 
58
58
  const response = await fetch(
59
- "http://localhost:3006/tasks",
59
+ "http://localhost:3006/projects",
60
60
  {
61
61
  body,
62
62
  method: "POST"
63
63
  }
64
64
  )
65
65
  const text = await response.text()
66
- const createdTask = await Task.last()
66
+ const createdProject = await Project.preload({translations: true}).last()
67
67
 
68
68
  expect(text).toEqual('{"status":"success"}')
69
- expect(createdTask.readAttribute("name")).toEqual("Test create task")
69
+ expect(createdProject.name()).toEqual("Test create project")
70
70
  })
71
71
  })
72
72
  })
@@ -15,16 +15,18 @@ export default class DbCreate extends BaseCommand{
15
15
  this.databaseConnection = await this.databasePool.spawnConnectionWithConfiguration(this.newConfiguration)
16
16
  await this.databaseConnection.connect()
17
17
 
18
- this.createDatabase()
19
- await this.createSchemaMigrationsTable()
18
+ if (this.configuration.getDatabaseType() != "sqlite") {
19
+ await this.createDatabase()
20
+ }
20
21
 
22
+ await this.createSchemaMigrationsTable()
21
23
  await this.databaseConnection.close()
22
24
 
23
25
  if (this.args.testing) return this.result
24
26
  }
25
27
 
26
28
  async createDatabase() {
27
- const databaseName = digg(this.databasePool.getConfiguration(), "database")
29
+ const databaseName = digg(this, "configuration", "database", "default", "master", "database")
28
30
  const sql = this.databaseConnection.createDatabaseSql(databaseName, {ifNotExists: true})
29
31
 
30
32
  if (this.args.testing) {
package/src/cli/index.js CHANGED
@@ -57,4 +57,6 @@ export default class VelociousCli {
57
57
  this.configuration.setCurrent()
58
58
  this.args.configuration = this.configuration
59
59
  }
60
+
61
+ getConfiguration = () => this.args.configuration
60
62
  }
@@ -42,6 +42,16 @@ export default class VelociousConfiguration {
42
42
  return poolTypeClass
43
43
  }
44
44
 
45
+ getDatabaseType() {
46
+ const databaseType = digg(this, "database", "default", "master", "type")
47
+
48
+ if (!databaseType) {
49
+ throw new Error("No database type given in database configuration")
50
+ }
51
+
52
+ return databaseType
53
+ }
54
+
45
55
  getDirectory() {
46
56
  if (!this._directory) {
47
57
  this._directory = process.cwd()
@@ -2,8 +2,9 @@ import Query from "../query/index.js"
2
2
  import Handler from "../handler.js"
3
3
 
4
4
  export default class VelociousDatabaseDriversBase {
5
- constructor(args) {
6
- this._args = args
5
+ constructor(config, configuration) {
6
+ this._args = config
7
+ this.configuration = configuration
7
8
  }
8
9
 
9
10
  async createTable(...args) {
@@ -24,6 +25,8 @@ export default class VelociousDatabaseDriversBase {
24
25
  return this._args
25
26
  }
26
27
 
28
+ getConfiguration = () => this.configuration
29
+
27
30
  getIdSeq() {
28
31
  return this.idSeq
29
32
  }
@@ -0,0 +1,13 @@
1
+ import {digg} from "diggerize"
2
+
3
+ export default class VelociousDatabaseDriversMysqlForeignKey {
4
+ constructor(data) {
5
+ this.data = data
6
+ }
7
+
8
+ getColumnName = () => digg(this, "data", "COLUMN_NAME")
9
+ getName = () => digg(this, "data", "CONSTRAINT_NAME")
10
+ getTableName = () => digg(this, "data", "TABLE_NAME")
11
+ getReferencedColumnName = () => digg(this, "data", "REFERENCED_COLUMN_NAME")
12
+ getReferencedTableName = () => digg(this, "data", "REFERENCED_TABLE_NAME")
13
+ }
@@ -1,6 +1,7 @@
1
1
  import Base from "../base.js"
2
2
  import connectConnection from "./connect-connection.js"
3
3
  import CreateDatabase from "./sql/create-database.js"
4
+ import CreateIndex from "./sql/create-index.js"
4
5
  import CreateTable from "./sql/create-table.js"
5
6
  import Delete from "./sql/delete.js"
6
7
  import {digg} from "diggerize"
@@ -50,6 +51,13 @@ export default class VelociousDatabaseDriversMysql extends Base{
50
51
  return createDatabase.toSql()
51
52
  }
52
53
 
54
+ createIndexSql(indexData) {
55
+ const createArgs = Object.assign({driver: this}, indexData)
56
+ const createIndex = new CreateIndex(createArgs)
57
+
58
+ return createIndex.toSql()
59
+ }
60
+
53
61
  createTableSql(tableData) {
54
62
  const createArgs = Object.assign({tableData, driver: this})
55
63
  const createTable = new CreateTable(createArgs)
@@ -57,6 +65,9 @@ export default class VelociousDatabaseDriversMysql extends Base{
57
65
  return createTable.toSql()
58
66
  }
59
67
 
68
+ getType = () => "mysql"
69
+ primaryKeyType = () => "bigint"
70
+
60
71
  async query(sql) {
61
72
  return await query(this.connection, sql)
62
73
  }
@@ -65,6 +76,8 @@ export default class VelociousDatabaseDriversMysql extends Base{
65
76
  return new QueryParser({query}).toSql()
66
77
  }
67
78
 
79
+ shouldSetAutoIncrementWhenPrimaryKey = () => true
80
+
68
81
  escape(string) {
69
82
  if (!this.connection) throw new Error("Can't escape before connected")
70
83
 
@@ -0,0 +1,4 @@
1
+ import CreateIndexBase from "../../../query/create-index-base.js"
2
+
3
+ export default class VelociousDatabaseConnectionDriversMysqlSqlCreateIndex extends CreateIndexBase {
4
+ }
@@ -1,4 +1,5 @@
1
1
  import Column from "./column.js"
2
+ import ForeignKey from "./foreign-key.js"
2
3
 
3
4
  export default class VelociousDatabaseDriversMysqlTable {
4
5
  constructor(driver, data) {
@@ -19,6 +20,27 @@ export default class VelociousDatabaseDriversMysqlTable {
19
20
  return columns
20
21
  }
21
22
 
23
+ async getForeignKeys() {
24
+ const sql = `
25
+ SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
26
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
27
+ WHERE
28
+ REFERENCED_TABLE_SCHEMA = (SELECT DATABASE()) AND
29
+ TABLE_NAME = ${this.driver.quote(this.getName())}
30
+ `
31
+
32
+ const foreignKeyRows = await this.driver.query(sql)
33
+ const foreignKeys = []
34
+
35
+ for (const foreignKeyRow of foreignKeyRows) {
36
+ const foreignKey = new ForeignKey(foreignKeyRow)
37
+
38
+ foreignKeys.push(foreignKey)
39
+ }
40
+
41
+ return foreignKeys
42
+ }
43
+
22
44
  getName() {
23
45
  return Object.values(this.data)[0]
24
46
  }
@@ -35,6 +35,7 @@ export default class VelociousDatabaseDriversSqliteBase extends Base {
35
35
  }
36
36
 
37
37
  deleteSql = (args) => new Delete(Object.assign({driver: this}, args)).toSql()
38
+ getType = () => "sqlite"
38
39
  insertSql = (args) => new Insert(Object.assign({driver: this}, args)).toSql()
39
40
 
40
41
  async getTableByName(tableName) {
@@ -125,6 +126,7 @@ export default class VelociousDatabaseDriversSqliteBase extends Base {
125
126
  return this._options
126
127
  }
127
128
 
129
+ primaryKeyType = () => "integer" // Because bigint on SQLite doesn't support auto increment
128
130
  queryToSql = (query) => new QueryParser({query}).toSql()
129
131
 
130
132
  async registerVersion() {
@@ -139,6 +141,8 @@ export default class VelociousDatabaseDriversSqliteBase extends Base {
139
141
  this.versionPatch = versionParts[2]
140
142
  }
141
143
 
144
+ shouldSetAutoIncrementWhenPrimaryKey = () => false
145
+
142
146
  escape(value) {
143
147
  const type = typeof value
144
148
 
@@ -0,0 +1,14 @@
1
+ import {digg} from "diggerize"
2
+
3
+ export default class VelociousDatabaseDriversSqliteForeignKey {
4
+ constructor(data, {tableName}) {
5
+ this.data = data
6
+ this.tableName = tableName
7
+ }
8
+
9
+ getColumnName = () => digg(this, "data", "from")
10
+ getName = () => `${this.getTableName()}_${this.getColumnName()}_${this.data.id}`
11
+ getTableName = () => digg(this, "tableName")
12
+ getReferencedColumnName = () => digg(this, "data", "to")
13
+ getReferencedTableName = () => digg(this, "data", "table")
14
+ }
@@ -1,4 +1,3 @@
1
- import debounce from "debounce"
2
1
  import {digg} from "diggerize"
3
2
  import fs from "fs/promises"
4
3
  import query from "./query.js"
@@ -10,7 +9,7 @@ import Base from "./base.js"
10
9
  export default class VelociousDatabaseDriversSqliteNode extends Base {
11
10
  async connect() {
12
11
  const args = this.getArgs()
13
- const databasePath = `db/${this.localStorageName()}.sqlite`
12
+ const databasePath = `${this.getConfiguration().getDirectory()}/db/${this.localStorageName()}.sqlite`
14
13
 
15
14
  if (args.reset) {
16
15
  await fs.unlink(databasePath)
@@ -1,4 +1,4 @@
1
1
  import CreateIndexBase from "../../../query/create-index-base.js"
2
2
 
3
- export default class VelociousDatabaseConnectionDriversMysqlSqlCreateIndex extends CreateIndexBase {
3
+ export default class VelociousDatabaseConnectionDriversSqliteSqlCreateIndex extends CreateIndexBase {
4
4
  }
@@ -1,5 +1,6 @@
1
1
  import Column from "./column.js"
2
2
  import {digg} from "diggerize"
3
+ import ForeignKey from "./foreign-key.js"
3
4
 
4
5
  export default class VelociousDatabaseDriversSqliteTable {
5
6
  constructor({driver, row}) {
@@ -7,7 +8,7 @@ export default class VelociousDatabaseDriversSqliteTable {
7
8
  this.row = row
8
9
  }
9
10
 
10
- getColumns = async () => {
11
+ async getColumns() {
11
12
  const result = await this.driver.query(`PRAGMA table_info('${this.getName()}')`)
12
13
  const columns = []
13
14
 
@@ -20,5 +21,18 @@ export default class VelociousDatabaseDriversSqliteTable {
20
21
  return columns
21
22
  }
22
23
 
24
+ async getForeignKeys() {
25
+ const foreignKeysData = await this.driver.query(`SELECT * FROM pragma_foreign_key_list(${this.driver.quote(this.getName())})`)
26
+ const foreignKeys = []
27
+
28
+ for (const foreignKeyData of foreignKeysData) {
29
+ const foreignKey = new ForeignKey(foreignKeyData, {tableName: this.getName()})
30
+
31
+ foreignKeys.push(foreignKey)
32
+ }
33
+
34
+ return foreignKeys
35
+ }
36
+
23
37
  getName = () => digg(this, "row", "name")
24
38
  }
@@ -9,7 +9,11 @@ export default class VelociousDatabaseInitializerFromRequireContext {
9
9
 
10
10
  async initialize({configuration}) {
11
11
  for (const fileName of this.requireContext.keys()) {
12
- const modelClass = this.requireContext(fileName).default
12
+ const modelClassImport = this.requireContext(fileName)
13
+
14
+ if (!modelClassImport) throw new Error(`Couldn't import model class from ${fileName}`)
15
+
16
+ const modelClass = modelClassImport.default
13
17
 
14
18
  await modelClass.initializeRecord({configuration})
15
19
 
@@ -1,3 +1,4 @@
1
+ import * as inflection from "inflection"
1
2
  import TableData, {TableColumn} from "../table-data/index.js"
2
3
 
3
4
  export default class VelociousDatabaseMigration {
@@ -5,10 +6,12 @@ export default class VelociousDatabaseMigration {
5
6
  this.configuration = configuration
6
7
  }
7
8
 
8
- async addColumn(tableName, columnName, args) {
9
+ async addColumn(tableName, columnName, columnType, args) {
9
10
  const databasePool = this.configuration.getDatabasePool()
11
+ const tableColumnArgs = Object.assign({type: columnType}, args)
12
+
10
13
  const sqls = databasePool.alterTableSql({
11
- columns: [new TableColumn(columnName, args)],
14
+ columns: [new TableColumn(columnName, tableColumnArgs)],
12
15
  tableName
13
16
  })
14
17
 
@@ -31,14 +34,46 @@ export default class VelociousDatabaseMigration {
31
34
  await databasePool.query(sql)
32
35
  }
33
36
 
37
+ async addForeignKey(tableName, referenceName) {
38
+ const referenceNameUnderscore = inflection.underscore(referenceName)
39
+ const tableNameUnderscore = inflection.underscore(tableName)
40
+ const columnName = `${referenceNameUnderscore}_id`
41
+ const databasePool = this.configuration.getDatabasePool()
42
+ const foreignKeyName = `fk_${tableName}_${referenceName}`
43
+ let sql = ""
44
+
45
+ sql += `ALTER TABLE ${databasePool.quoteTable(tableName)}`
46
+ sql += ` ADD CONSTRAINT ${foreignKeyName} `
47
+ sql += ` FOREIGN KEY (${databasePool.quoteColumn(columnName)})`
48
+ sql += ` REFERENCES ${tableNameUnderscore}(id)`
49
+
50
+ await databasePool.query(sql)
51
+ }
52
+
53
+ async addReference(tableName, referenceName, args) {
54
+ const columnName = `${inflection.underscore(referenceName)}_id`
55
+
56
+ await this.addColumn(tableName, columnName, {type: args?.type})
57
+ await this.addIndex(tableName, [columnName], {unique: args?.unique})
58
+
59
+ if (args?.foreignKey) {
60
+ await this.addForeignKey(tableName, referenceName)
61
+ }
62
+ }
63
+
34
64
  async createTable(tableName, callback) {
65
+ const databasePool = this.configuration.getDatabasePool()
66
+ const primaryKeyType = databasePool.primaryKeyType()
35
67
  const tableData = new TableData(tableName)
36
68
 
37
- tableData.integer("id", {null: false, primaryKey: true})
69
+ if (!(primaryKeyType in tableData)) throw new Error(`Unsupported primary key type: ${primaryKeyType}`)
38
70
 
39
- callback(tableData)
71
+ tableData[primaryKeyType]("id", {autoIncrement: true, null: false, primaryKey: true})
72
+
73
+ if (callback) {
74
+ callback(tableData)
75
+ }
40
76
 
41
- const databasePool = this.configuration.getDatabasePool()
42
77
  const sqls = databasePool.createTableSql(tableData)
43
78
 
44
79
  for (const sql of sqls) {
@@ -4,14 +4,6 @@ import BasePool from "./base.js"
4
4
  let idSeq = 0
5
5
 
6
6
  export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends BasePool {
7
- static current() {
8
- if (!this.velociousDatabasePoolAsyncTrackedMultiConnection) {
9
- this.velociousDatabasePoolAsyncTrackedMultiConnection = new VelociousDatabasePoolAsyncTrackedMultiConnection()
10
- }
11
-
12
- return this.velociousDatabasePoolAsyncTrackedMultiConnection
13
- }
14
-
15
7
  constructor(args = {}) {
16
8
  super(args)
17
9
  this.connections = []
@@ -48,10 +40,6 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
48
40
  return connection
49
41
  }
50
42
 
51
- setCurrent() {
52
- this.constructor.velociousDatabasePoolAsyncTrackedMultiConnection = this
53
- }
54
-
55
43
  async withConnection(callback) {
56
44
  const connection = await this.checkout()
57
45
  const id = connection.getIdSeq()
@@ -1,7 +1,21 @@
1
1
  import Configuration from "../../configuration.js"
2
2
  import {digg} from "diggerize"
3
3
 
4
+ if (!globalThis.velociousDatabasePoolBase) {
5
+ globalThis.velociousDatabasePoolBase = {
6
+ current: null
7
+ }
8
+ }
9
+
4
10
  class VelociousDatabasePoolBase {
11
+ static current() {
12
+ if (!globalThis.velociousDatabasePoolBase.current) {
13
+ globalThis.velociousDatabasePoolBase.current = new this()
14
+ }
15
+
16
+ return globalThis.velociousDatabasePoolBase.current
17
+ }
18
+
5
19
  constructor(args = {}) {
6
20
  this.configuration = args.configuration || Configuration.current()
7
21
  this.connections = []
@@ -10,6 +24,10 @@ class VelociousDatabasePoolBase {
10
24
 
11
25
  getConfiguration = () => digg(this, "configuration", "database", "default", "master")
12
26
 
27
+ setCurrent() {
28
+ globalThis.velociousDatabasePoolBase.current = this
29
+ }
30
+
13
31
  setDriverClass(driverClass) {
14
32
  this.driverClass = driverClass
15
33
  }
@@ -26,7 +44,7 @@ class VelociousDatabasePoolBase {
26
44
 
27
45
  if (!DriverClass) throw new Error("No driver class set in database pool or in given config")
28
46
 
29
- const connection = new DriverClass(config)
47
+ const connection = new DriverClass(config, this.configuration)
30
48
 
31
49
  await connection.connect()
32
50
 
@@ -46,8 +64,11 @@ const forwardMethods = [
46
64
  "getTables",
47
65
  "insert",
48
66
  "insertSql",
67
+ "primaryKeyType",
49
68
  "query",
50
69
  "quote",
70
+ "quoteColumn",
71
+ "quoteTable",
51
72
  "select",
52
73
  "update",
53
74
  "updateSql"
@@ -1,14 +1,6 @@
1
1
  import BasePool from "./base.js"
2
2
 
3
3
  export default class VelociousDatabasePoolSingleMultiUser extends BasePool {
4
- static current() {
5
- if (!this.velociousDatabasePoolSingleMultiUser) {
6
- this.velociousDatabasePoolSingleMultiUser = new VelociousDatabasePoolSingleMultiUser()
7
- }
8
-
9
- return this.velociousDatabasePoolSingleMultiUser
10
- }
11
-
12
4
  checkin = (connection) => {
13
5
  // Do nothing
14
6
  }
@@ -21,10 +13,6 @@ export default class VelociousDatabasePoolSingleMultiUser extends BasePool {
21
13
  return this.connection
22
14
  }
23
15
 
24
- setCurrent() {
25
- this.constructor.velociousDatabasePoolSingleMultiUser = this
26
- }
27
-
28
16
  async withConnection(callback) {
29
17
  await this.checkout() // Ensure a connection is present
30
18
  await callback(this.connection)
@@ -15,7 +15,7 @@ export default class VelociousDatabaseQueryCreateIndexBase extends QueryBase {
15
15
  let indexName = `index_on_${this.tableName}_`
16
16
 
17
17
  for (const columnIndex in this.columns) {
18
- if (columnIndex > 0) indexName += "_"
18
+ if (columnIndex > 0) indexName += "_and_"
19
19
 
20
20
  indexName += this.columns[columnIndex]
21
21
  }
@@ -1,4 +1,5 @@
1
1
  import CreateIndexBase from "./create-index-base.js"
2
+ import * as inflection from "inflection"
2
3
  import QueryBase from "./base.js"
3
4
 
4
5
  export default class VelociousDatabaseQueryCreateTableBase extends QueryBase {
@@ -9,7 +10,10 @@ export default class VelociousDatabaseQueryCreateTableBase extends QueryBase {
9
10
  this.tableData = tableData
10
11
  }
11
12
 
13
+ getConfiguration = () => this.driver.getConfiguration()
14
+
12
15
  toSql() {
16
+ const databaseType = this.getConfiguration().getDatabaseType()
13
17
  const {tableData} = this
14
18
  const sqls = []
15
19
 
@@ -24,23 +28,40 @@ export default class VelociousDatabaseQueryCreateTableBase extends QueryBase {
24
28
  for (const column of tableData.getColumns()) {
25
29
  columnCount++
26
30
 
27
- let maxlength = column.args.maxlength
28
- let type = column.args.type
31
+ let maxlength = column.getMaxLength()
32
+ let type = column.getType().toUpperCase()
29
33
 
30
- if (type == "string") {
31
- type = "varchar"
34
+ if (type == "STRING") {
35
+ type = "VARCHAR"
32
36
  maxlength ||= 255
33
37
  }
34
38
 
39
+ if (databaseType == "sqlite" && column.getAutoIncrement() && column.getPrimaryKey()) {
40
+ type = "INTEGER"
41
+ }
42
+
35
43
  if (columnCount > 1) sql += ", "
36
44
 
37
- sql += `${this.driver.quoteColumn(column.name)} ${type}`
45
+ sql += `${this.driver.quoteColumn(column.getName())} ${type}`
38
46
 
39
47
  if (maxlength !== undefined) sql += `(${maxlength})`
40
48
 
41
- if (column.args.autoIncrement) sql += " AUTO_INCREMENT"
42
- if (column.args.primaryKey) sql += " PRIMARY KEY"
43
- if (column.args.null === false) sql += " NOT NULL"
49
+ if (column.getAutoIncrement() && this.driver.shouldSetAutoIncrementWhenPrimaryKey()) sql += " AUTO_INCREMENT"
50
+ if (column.getPrimaryKey()) sql += " PRIMARY KEY"
51
+ if (column.getNull() === false) sql += " NOT NULL"
52
+
53
+ if (column.getForeignKey()) {
54
+ let foreignKeyTable, foreignKeyColumn
55
+
56
+ if (column.getForeignKey() === true) {
57
+ foreignKeyColumn = "id"
58
+ foreignKeyTable = inflection.pluralize(column.getName().replace(/_id$/, ""))
59
+ } else {
60
+ throw new Error(`Unknown foreign key type given: ${column.getForeignKey()} (${typeof column.getForeignKey()})`)
61
+ }
62
+
63
+ sql += ` REFERENCES ${this.driver.quoteTable(foreignKeyTable)}(${this.driver.quoteColumn(foreignKeyColumn)})`
64
+ }
44
65
  }
45
66
 
46
67
  if (this.indexInCreateTable) {
@@ -47,6 +47,14 @@ export default class VelociousDatabaseQuery {
47
47
 
48
48
  getOptions = () => this.driver.options()
49
49
 
50
+ async destroyAll() {
51
+ const records = await this.toArray()
52
+
53
+ for (const record of records) {
54
+ await record.destroy()
55
+ }
56
+ }
57
+
50
58
  async find(recordId) {
51
59
  const conditions = {}
52
60
 
@@ -20,7 +20,7 @@ export default class VelociousDatabaseQueryInsertBase {
20
20
  }
21
21
 
22
22
  toSql() {
23
- let sql = `INSERT INTO ${this.getOptions().quoteTableName(this.tableName)} (`
23
+ let sql = `INSERT INTO ${this.getOptions().quoteTableName(this.tableName)}`
24
24
  let count = 0
25
25
  let columns
26
26
 
@@ -32,16 +32,24 @@ export default class VelociousDatabaseQueryInsertBase {
32
32
  throw new Error("Neither 'column' and 'rows' or data was given")
33
33
  }
34
34
 
35
- for (const columnName of columns) {
36
- if (count > 0) sql += ", "
35
+ if (columns.length > 0) {
36
+ sql += " ("
37
37
 
38
- sql += this.getOptions().quoteColumnName(columnName)
39
- count++
40
- }
38
+ for (const columnName of columns) {
39
+ if (count > 0) sql += ", "
40
+
41
+ sql += this.getOptions().quoteColumnName(columnName)
42
+ count++
43
+ }
41
44
 
42
- sql += ") VALUES "
45
+ sql += ")"
46
+ }
43
47
 
44
48
  if (this.columns && this.rows) {
49
+ if (this.rows.length > 0) {
50
+ sql += " VALUES "
51
+ }
52
+
45
53
  let count = 0
46
54
 
47
55
  for (const row of this.rows) {
@@ -51,7 +59,14 @@ export default class VelociousDatabaseQueryInsertBase {
51
59
  sql += this._valuesSql(row)
52
60
  }
53
61
  } else {
54
- sql += this._valuesSql(Object.values(this.data))
62
+ if (Object.keys(this.data).length > 0) {
63
+ sql += " VALUES "
64
+ sql += this._valuesSql(Object.values(this.data))
65
+ } else if (this.driver.getType() == "sqlite") {
66
+ sql += " DEFAULT VALUES"
67
+ } else if (this.driver.getType() == "mysql") {
68
+ sql += " () VALUES ()"
69
+ }
55
70
  }
56
71
 
57
72
  return sql
@@ -457,6 +457,10 @@ export default class VelociousDatabaseRecord {
457
457
  return this._newQuery()
458
458
  }
459
459
 
460
+ static async destroyAll(...args) {
461
+ return this._newQuery().destroyAll(...args)
462
+ }
463
+
460
464
  static async find(...args) {
461
465
  return this._newQuery().find(...args)
462
466
  }
@@ -1,14 +1,34 @@
1
+ import restArgsError from "../../utils/rest-args-error.js"
2
+
1
3
  class TableColumn {
2
4
  constructor(name, args) {
5
+ if (args) {
6
+ const {autoIncrement, foreignKey, maxLength, name, null: argsNull, primaryKey, type, ...restArgs} = args
7
+
8
+ restArgsError(restArgs)
9
+ }
10
+
3
11
  this.args = args
4
12
  this.name = name
5
13
  }
6
14
 
15
+ getAutoIncrement = () => this.args?.autoIncrement
16
+ getForeignKey = () => this.args?.foreignKey
17
+ getMaxLength = () => this.args?.maxLength
7
18
  getName = () => this.name
19
+ getNull = () => this.args?.null
20
+ getPrimaryKey = () => this.args?.primaryKey
21
+ getType = () => this.args?.type
8
22
  }
9
23
 
10
24
  class TableIndex {
11
25
  constructor(columns, args) {
26
+ if (args) {
27
+ const {name, unique, ...restArgs} = args
28
+
29
+ restArgsError(restArgs)
30
+ }
31
+
12
32
  this.args = args
13
33
  this.columns = columns
14
34
  }