velocious 1.0.35 → 1.0.37

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/README.md +1 -0
  2. package/package.json +5 -3
  3. package/peak_flow.yml +22 -2
  4. package/spec/cli/commands/db/create-spec.js +6 -0
  5. package/spec/database/drivers/mysql/connection-spec.js +1 -1
  6. package/spec/database/record/create-spec.js +24 -0
  7. package/spec/database/record/update-spec.js +15 -0
  8. package/spec/database/record/validations-spec.js +28 -0
  9. package/spec/database/transactions-spec.js +36 -0
  10. package/spec/dummy/src/config/{configuration.sqlite.js → configuration.peakflow.pgsql.js} +11 -38
  11. package/spec/dummy/src/models/task.js +1 -0
  12. package/src/cli/commands/db/create.js +15 -10
  13. package/src/cli/commands/test.js +8 -0
  14. package/src/configuration.js +14 -0
  15. package/src/database/drivers/base.js +21 -4
  16. package/src/database/drivers/mssql/index.js +12 -3
  17. package/src/database/drivers/mssql/sql/create-database.js +1 -1
  18. package/src/database/drivers/mysql/index.js +2 -2
  19. package/src/database/drivers/pgsql/column.js +10 -0
  20. package/src/database/drivers/pgsql/foreign-key.js +13 -0
  21. package/src/database/drivers/pgsql/index.js +172 -0
  22. package/src/database/drivers/pgsql/options.js +18 -0
  23. package/src/database/drivers/pgsql/query-parser.js +4 -0
  24. package/src/database/drivers/pgsql/sql/create-database.js +37 -0
  25. package/src/database/drivers/pgsql/sql/create-index.js +4 -0
  26. package/src/database/drivers/pgsql/sql/create-table.js +4 -0
  27. package/src/database/drivers/pgsql/sql/delete.js +19 -0
  28. package/src/database/drivers/pgsql/sql/drop-table.js +4 -0
  29. package/src/database/drivers/pgsql/sql/insert.js +4 -0
  30. package/src/database/drivers/pgsql/sql/update.js +31 -0
  31. package/src/database/drivers/pgsql/table.js +62 -0
  32. package/src/database/drivers/sqlite/base.js +4 -0
  33. package/src/database/drivers/sqlite/column.js +10 -0
  34. package/src/database/migrator.js +1 -1
  35. package/src/database/query/create-database-base.js +1 -1
  36. package/src/database/query/create-table-base.js +16 -2
  37. package/src/database/query/index.js +10 -1
  38. package/src/database/query/insert-base.js +1 -1
  39. package/src/database/record/index.js +162 -18
  40. package/src/database/record/validators/base.js +2 -0
  41. package/src/database/record/validators/presence.js +13 -0
  42. package/src/database/record/validators/uniqueness.js +23 -0
  43. package/src/testing/test-runner.js +10 -0
  44. package/spec/dummy/src/config/configuration.mariadb.js +0 -97
package/README.md CHANGED
@@ -32,6 +32,7 @@ class Task extends Record {
32
32
 
33
33
  Task.belongsTo("account")
34
34
  Task.translates("description", "subTitle", "title")
35
+ Task.validates("name", {presence: true, uniqueness: true})
35
36
 
36
37
  export default Task
37
38
  ```
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "velocious": "bin/velocious.js"
4
4
  },
5
5
  "name": "velocious",
6
- "version": "1.0.35",
6
+ "version": "1.0.37",
7
7
  "main": "index.js",
8
8
  "scripts": {
9
9
  "test": "jasmine",
@@ -22,10 +22,12 @@
22
22
  "homepage": "https://github.com/kaspernj/velocious#readme",
23
23
  "description": "",
24
24
  "devDependencies": {
25
+ "awaitery": "^1.0.1",
25
26
  "jasmine": "^5.0.2",
26
27
  "mssql": "^11.0.1",
27
28
  "mysql": "^2.18.1",
28
29
  "node-fetch": "^3.3.1",
30
+ "pg": "^8.16.3",
29
31
  "require-context": "^1.1.0",
30
32
  "sqlite": "^5.1.1",
31
33
  "sqlite3": "^5.1.7",
@@ -42,9 +44,9 @@
42
44
  "escape-string-regexp": "^1.0.5",
43
45
  "incorporator": "^1.0.2",
44
46
  "inflection": "^3.0.0",
47
+ "pure-uuid": "^1.8.1",
45
48
  "sql-escape-string": "^1.1.0",
46
49
  "sql.js": "^1.12.0",
47
- "strftime": "^0.10.2",
48
- "uuid": "^12.0.0"
50
+ "strftime": "^0.10.2"
49
51
  }
50
52
  }
package/peak_flow.yml CHANGED
@@ -30,24 +30,44 @@ services:
30
30
  - 1433
31
31
  mem_limit: 4096m
32
32
  restart_policy: on-failure
33
+ postgres:
34
+ environment:
35
+ POSTGRES_USER: peakflow
36
+ POSTGRES_PASSWORD: password
37
+ POSTGRES_DB: velocious_test
38
+ image: postgres:17.6
39
+ expose:
40
+ - 5432
41
+ mem_limit: 4096m
42
+ restart_policy: on-failure
33
43
  builds:
34
44
  build_1:
35
45
  name: MariaDB
36
46
  script:
37
47
  - cp spec/dummy/src/config/configuration.peakflow.mariadb.js spec/dummy/src/config/configuration.js
38
- - cd spec/dummy && npx velocious db:create
39
48
  - wait-for-it mariadb:3306
49
+ - wait-for-it mssql:1433
50
+ - cd spec/dummy && npx velocious db:create
40
51
  - npm test
41
52
  build_2:
42
53
  name: MS-SQL
43
54
  script:
44
- - wait-for-it mssql:1433
45
55
  - cp spec/dummy/src/config/configuration.peakflow.mssql.js spec/dummy/src/config/configuration.js
56
+ - wait-for-it mssql:1433
46
57
  - cd spec/dummy && npx velocious db:create
47
58
  - npm test
48
59
  build_3:
60
+ name: PG-SQL
61
+ script:
62
+ - cp spec/dummy/src/config/configuration.peakflow.pgsql.js spec/dummy/src/config/configuration.js
63
+ - wait-for-it mssql:1433
64
+ - wait-for-it postgres:5432
65
+ - cd spec/dummy && npx velocious db:create
66
+ - npm test
67
+ build_4:
49
68
  name: SQLite
50
69
  script:
51
70
  - cp spec/dummy/src/config/configuration.peakflow.sqlite.js spec/dummy/src/config/configuration.js
71
+ - wait-for-it mssql:1433
52
72
  - cd spec/dummy && npx velocious db:create
53
73
  - npm test
@@ -30,6 +30,12 @@ describe("Cli - Commands - db:create", () => {
30
30
  }
31
31
  ]
32
32
  )
33
+ } else if (cli.getConfiguration().getDatabaseType() == "pgsql") {
34
+ expect(result[2]).toEqual(
35
+ {
36
+ createSchemaMigrationsTableSql: 'CREATE TABLE IF NOT EXISTS "schema_migrations" ("version" VARCHAR(255) PRIMARY KEY NOT NULL)'
37
+ }
38
+ )
33
39
  } else {
34
40
  expect(result).toEqual(
35
41
  [
@@ -6,7 +6,7 @@ const mysqlConfig = digg(configuration, "database", "test", "default")
6
6
 
7
7
  describe("Database - Drivers - Mysql - Connection", () => {
8
8
  it("connects", async () => {
9
- if (configuration.getDatabaseType() != "sqlite" && configuration.getDatabaseType() != "mssql") {
9
+ if (configuration.getDatabaseType() != "sqlite" && configuration.getDatabaseType() != "mssql" && configuration.getDatabaseType() != "pgsql") {
10
10
  const mysql = new DatabaseDriversMysql(mysqlConfig)
11
11
 
12
12
  await mysql.connect()
@@ -19,6 +19,8 @@ describe("Record - create", () => {
19
19
  expect(project.name()).toEqual("Test project")
20
20
  expect(project.nameDe()).toEqual("Test projekt")
21
21
  expect(project.nameEn()).toEqual("Test project")
22
+ expect(project.createdAt()).toBeInstanceOf(Date)
23
+ expect(project.updatedAt()).toBeInstanceOf(Date)
22
24
  })
23
25
  })
24
26
 
@@ -35,4 +37,26 @@ describe("Record - create", () => {
35
37
  expect(task.project()).toEqual(project)
36
38
  })
37
39
  })
40
+
41
+ it("uses transactions and rolls back in case of an error", async () => {
42
+ await Dummy.run(async () => {
43
+ const project = new Project({name: "Test project"})
44
+
45
+ project.tasks().build({name: " ", project})
46
+
47
+ try {
48
+ await project.save()
49
+
50
+ throw new Error("Didnt expect to succeed")
51
+ } catch (error) {
52
+ expect(error.message).toEqual("Validation failed: Name can't be blank")
53
+ }
54
+
55
+ const projectsCount = await Project.count()
56
+ const tasksCount = await Task.count()
57
+
58
+ expect(projectsCount).toEqual(0)
59
+ expect(tasksCount).toEqual(0)
60
+ })
61
+ })
38
62
  })
@@ -1,5 +1,7 @@
1
1
  import Dummy from "../../dummy/index.js"
2
+ import Project from "../../dummy/src/models/project.js"
2
3
  import Task from "../../dummy/src/models/task.js"
4
+ import wait from "awaitery/src/wait.js"
3
5
 
4
6
  describe("Record - update", () => {
5
7
  it("updates a record", async () => {
@@ -12,4 +14,17 @@ describe("Record - update", () => {
12
14
  expect(task.readAttribute("name")).toEqual("Updated name")
13
15
  })
14
16
  })
17
+
18
+ it("updates a record with timestamps", async () => {
19
+ await Dummy.run(async () => {
20
+ const project = new Project({name: "Test project"})
21
+
22
+ await project.save()
23
+ await wait(1000)
24
+ await project.update({name: "Updated name"})
25
+
26
+ expect(project.name()).toEqual("Updated name")
27
+ expect(project.updatedAt()).not.toEqual(project.createdAt())
28
+ })
29
+ })
15
30
  })
@@ -0,0 +1,28 @@
1
+ import Dummy from "../../dummy/index.js"
2
+ import Task from "../../dummy/src/models/task.js"
3
+
4
+ describe("Record - validations", () => {
5
+ it("raises validations if trying to create an invalid record because of a presence validation", async () => {
6
+ await Dummy.run(async () => {
7
+ const task = new Task({name: " "})
8
+
9
+ await expectAsync(task.save()).toBeRejectedWith(new Error("Validation failed: Name can't be blank"))
10
+ })
11
+ })
12
+
13
+ it("raises validations if trying to create an invalid record because of a uniqueness validation", async () => {
14
+ await Dummy.run(async () => {
15
+ await Task.create({name: "Task 1"})
16
+
17
+ const task2 = await Task.create({name: "Task 2"})
18
+
19
+ try {
20
+ await task2.update({name: "Task 1"})
21
+
22
+ throw new Error("Task 2 save didn't fail")
23
+ } catch (error) {
24
+ expect(error.message).toEqual("Validation failed: Name has already been taken")
25
+ }
26
+ })
27
+ })
28
+ })
@@ -0,0 +1,36 @@
1
+ import Dummy from "../dummy/index.js"
2
+ import dummyConfiguration from "../dummy/src/config/configuration.js"
3
+ import Task from "../dummy/src/models/task.js"
4
+
5
+ describe("database - transactions", () => {
6
+ it("supports transactions and savepoints", async () => {
7
+ await Dummy.run(async () => {
8
+ await dummyConfiguration.withConnections(async (dbs) => {
9
+ const db = dbs.default
10
+
11
+ await db.transaction(async () => {
12
+ await Task.create({name: "Task 1"})
13
+
14
+ await db.transaction(async () => {
15
+ await Task.create({name: "Task 2"})
16
+
17
+ try {
18
+ await db.transaction(async () => {
19
+ await Task.create({name: "Task 3"})
20
+ await Task.create({name: " "})
21
+
22
+ throw new Error("Didn't expect to succeed")
23
+ })
24
+ } catch (error) {
25
+ expect(error.message).toEqual("Validation failed: Name can't be blank")
26
+ }
27
+ })
28
+ })
29
+ })
30
+
31
+ const tasksCount = await Task.count()
32
+
33
+ expect(tasksCount).toEqual(2)
34
+ })
35
+ })
36
+ })
@@ -5,60 +5,33 @@ import fs from "fs/promises"
5
5
  import InitializerFromRequireContext from "../../../../src/database/initializer-from-require-context.js"
6
6
  import MssqlDriver from "../../../../src/database/drivers/mssql/index.js"
7
7
  import path from "path"
8
+ import PgsqlDriver from "../../../../src/database/drivers/pgsql/index.js"
8
9
  import requireContext from "require-context"
9
- import SqliteDriver from "../../../../src/database/drivers/sqlite/index.js"
10
- import SingleMultiUsePool from "../../../../src/database/pool/single-multi-use.js"
11
10
 
12
11
  export default new Configuration({
13
12
  database: {
14
- development: {
15
- default: {
16
- driver: SqliteDriver,
17
- poolType: SingleMultiUsePool,
18
- type: "sqlite",
19
- name: "test-db-development"
20
- },
21
- mssql: {
22
- driver: MssqlDriver,
23
- poolType: AsyncTrackedMultiConnection,
24
- type: "mssql",
25
- database: "velocious_development",
26
- useDatabase: "velocious_development",
27
- sqlConfig: {
28
- user: "sa",
29
- password: "Super-Secret-Password",
30
- database: "velocious_development",
31
- server: "6.0.0.8",
32
- pool: {
33
- max: 10,
34
- min: 0,
35
- idleTimeoutMillis: 30000
36
- },
37
- options: {
38
- encrypt: true, // for azure
39
- trustServerCertificate: true // change to true for local dev / self-signed certs
40
- }
41
- }
42
- }
43
- },
44
13
  test: {
45
14
  default: {
46
- driver: SqliteDriver,
47
- poolType: SingleMultiUsePool,
48
- type: "sqlite",
49
- name: "test-db-test"
15
+ driver: PgsqlDriver,
16
+ poolType: AsyncTrackedMultiConnection,
17
+ type: "pgsql",
18
+ host: "postgres",
19
+ username: "peakflow",
20
+ password: "password",
21
+ database: "velocious_test",
22
+ useDatabase: "velocious_test"
50
23
  },
51
24
  mssql: {
52
25
  driver: MssqlDriver,
53
26
  poolType: AsyncTrackedMultiConnection,
54
27
  type: "mssql",
55
28
  database: "velocious_test",
56
- useDatabase: "velocious_test",
29
+ useDatabase: "default",
57
30
  sqlConfig: {
58
31
  user: "sa",
59
32
  password: "Super-Secret-Password",
60
33
  database: "velocious_test",
61
- server: "6.0.0.8",
34
+ server: "mssql",
62
35
  pool: {
63
36
  max: 10,
64
37
  min: 0,
@@ -4,5 +4,6 @@ class Task extends DatabaseRecord {
4
4
  }
5
5
 
6
6
  Task.belongsTo("project")
7
+ Task.validates("name", {presence: true, uniqueness: true})
7
8
 
8
9
  export default Task
@@ -24,12 +24,15 @@ export default class DbCreate extends BaseCommand{
24
24
  this.databaseConnection = await this.databasePool.spawnConnectionWithConfiguration(this.newConfiguration)
25
25
  await this.databaseConnection.connect()
26
26
 
27
- if (databaseType != "sqlite") {
28
- await this.createDatabase(databaseIdentifier)
29
- }
27
+ try {
28
+ if (databaseType != "sqlite") {
29
+ await this.createDatabase(databaseIdentifier)
30
+ }
30
31
 
31
- await this.createSchemaMigrationsTable()
32
- await this.databaseConnection.close()
32
+ await this.createSchemaMigrationsTable()
33
+ } finally {
34
+ await this.databaseConnection.close()
35
+ }
33
36
 
34
37
  if (this.args.testing) return this.result
35
38
  }
@@ -37,12 +40,14 @@ export default class DbCreate extends BaseCommand{
37
40
 
38
41
  async createDatabase(databaseIdentifier) {
39
42
  const databaseName = digg(this.configuration.getDatabaseConfiguration(), databaseIdentifier, "database")
40
- const sql = this.databaseConnection.createDatabaseSql(databaseName, {ifNotExists: true})
43
+ const sqls = this.databaseConnection.createDatabaseSql(databaseName, {ifNotExists: true})
41
44
 
42
- if (this.args.testing) {
43
- this.result.push({databaseName, sql})
44
- } else {
45
- await this.databaseConnection.query(sql)
45
+ for (const sql of sqls) {
46
+ if (this.args.testing) {
47
+ this.result.push({databaseName, sql})
48
+ } else {
49
+ await this.databaseConnection.query(sql)
50
+ }
46
51
  }
47
52
  }
48
53
 
@@ -9,5 +9,13 @@ export default class VelociousCliCommandsInit extends BaseCommand {
9
9
  const testRunner = new TestRunner({configuration: this.configuration, testFiles})
10
10
 
11
11
  await testRunner.run()
12
+
13
+ if (testRunner.isFailed()) {
14
+ console.error(`Test run failed with ${testRunner.failedTests} failed tests and ${testRunner.successfulTests} successfull`)
15
+ process.exit(-1)
16
+ } else {
17
+ console.log(`Test run succeeded with ${testRunner.successfulTests} successful tests`)
18
+ process.exit(1)
19
+ }
12
20
  }
13
21
  }
@@ -137,6 +137,20 @@ export default class VelociousConfiguration {
137
137
  this.routes = newRoutes
138
138
  }
139
139
 
140
+ setTranslator(callback) {
141
+ this._translator = callback
142
+ }
143
+
144
+ _defaultTranslator(msgID, args) {
145
+ if (args?.defaultValue) return args.defaultValue
146
+
147
+ return msgID
148
+ }
149
+
150
+ getTranslator() {
151
+ return this._translator || this._defaultTranslator
152
+ }
153
+
140
154
  async withConnections(callback) {
141
155
  const dbs = {}
142
156
  const actualCallback = async () => {
@@ -1,7 +1,8 @@
1
1
  import {Logger} from "../../logger.js"
2
2
  import Query from "../query/index.js"
3
3
  import Handler from "../handler.js"
4
- import {v4 as uuidv4} from "uuid"
4
+ import strftime from "strftime"
5
+ import UUID from "pure-uuid"
5
6
 
6
7
  export default class VelociousDatabaseDriversBase {
7
8
  constructor(config, configuration) {
@@ -66,6 +67,14 @@ export default class VelociousDatabaseDriversBase {
66
67
  throw new Error(`${this.constructor.name}#lastInsertID not implemented`)
67
68
  }
68
69
 
70
+ _convertValue(value) {
71
+ if (value instanceof Date) {
72
+ return strftime("%F %T.%L", value)
73
+ }
74
+
75
+ return value
76
+ }
77
+
69
78
  quote(value) {
70
79
  if (typeof value == "number") return value
71
80
 
@@ -126,9 +135,17 @@ export default class VelociousDatabaseDriversBase {
126
135
 
127
136
  if (this._transactionsCount == 0) {
128
137
  this.logger.debug("Start transaction")
129
- await this.startTransaction()
130
- transactionStarted = true
131
138
  this._transactionsCount++
139
+
140
+ try {
141
+ await this.startTransaction()
142
+ } catch (error) {
143
+ this._transactionsCount--
144
+
145
+ throw error
146
+ }
147
+
148
+ transactionStarted = true
132
149
  } else {
133
150
  this.logger.debug("Start savepoint", savePointName)
134
151
  await this.startSavePoint(savePointName)
@@ -183,7 +200,7 @@ export default class VelociousDatabaseDriversBase {
183
200
  }
184
201
 
185
202
  generateSavePointName() {
186
- return `sp${uuidv4().replaceAll("-", "")}`
203
+ return `sp${new UUID(4).format().replaceAll("-", "")}`
187
204
  }
188
205
 
189
206
  async startSavePoint(savePointName) {
@@ -12,7 +12,7 @@ import mssql from "mssql"
12
12
  import QueryParser from "./query-parser.js"
13
13
  import Table from "./table.js"
14
14
  import Update from "./sql/update.js"
15
- import {v4 as uuidv4} from "uuid"
15
+ import UUID from "pure-uuid"
16
16
 
17
17
  export default class VelociousDatabaseDriversMssql extends Base{
18
18
  async connect() {
@@ -99,7 +99,11 @@ export default class VelociousDatabaseDriversMssql extends Base{
99
99
 
100
100
  shouldSetAutoIncrementWhenPrimaryKey = () => true
101
101
 
102
+
103
+
102
104
  escape(value) {
105
+ value = this._convertValue(value)
106
+
103
107
  const type = typeof value
104
108
 
105
109
  if (type != "string") value = `${value}`
@@ -111,6 +115,8 @@ export default class VelociousDatabaseDriversMssql extends Base{
111
115
  }
112
116
 
113
117
  quote(value) {
118
+ value = this._convertValue(value)
119
+
114
120
  const type = typeof value
115
121
 
116
122
  if (type == "number") return value
@@ -172,9 +178,11 @@ export default class VelociousDatabaseDriversMssql extends Base{
172
178
  }
173
179
 
174
180
  async startTransaction() {
181
+ if (!this.connection) throw new Error("No connection")
175
182
  if (this._currentTransaction) throw new Error("A transaction is already running")
176
183
 
177
- this._currentTransaction = new mssql.Transaction()
184
+ this._currentTransaction = new mssql.Transaction(this.connection)
185
+
178
186
  await this._currentTransaction.begin()
179
187
  }
180
188
 
@@ -189,6 +197,7 @@ export default class VelociousDatabaseDriversMssql extends Base{
189
197
  if (!this._currentTransaction) throw new Error("A transaction isn't running")
190
198
 
191
199
  await this._currentTransaction.rollback()
200
+
192
201
  this._currentTransaction = null
193
202
  }
194
203
 
@@ -205,7 +214,7 @@ export default class VelociousDatabaseDriversMssql extends Base{
205
214
  }
206
215
 
207
216
  generateSavePointName() {
208
- return `sp${uuidv4().replaceAll("-", "")}`.substring(0, 32)
217
+ return `sp${new UUID(4).format().replaceAll("-", "")}`.substring(0, 32)
209
218
  }
210
219
 
211
220
  updateSql({conditions, data, tableName}) {
@@ -23,6 +23,6 @@ export default class VelociousDatabaseConnectionDriversMssqlSqlCreateDatabase ex
23
23
  sql += " END"
24
24
  }
25
25
 
26
- return sql
26
+ return [sql]
27
27
  }
28
28
  }
@@ -102,7 +102,7 @@ export default class VelociousDatabaseDriversMysql extends Base{
102
102
  escape(value) {
103
103
  if (!this.connection) throw new Error("Can't escape before connected")
104
104
 
105
- const escapedValueWithQuotes = this.connection.escape(value)
105
+ const escapedValueWithQuotes = this.connection.escape(this._convertValue(value))
106
106
 
107
107
  return escapedValueWithQuotes.slice(1, escapedValueWithQuotes.length - 1)
108
108
  }
@@ -110,7 +110,7 @@ export default class VelociousDatabaseDriversMysql extends Base{
110
110
  quote(value) {
111
111
  if (!this.connection) throw new Error("Can't escape before connected")
112
112
 
113
- return this.connection.escape(value)
113
+ return this.connection.escape(this._convertValue(value))
114
114
  }
115
115
 
116
116
  deleteSql({tableName, conditions}) {
@@ -0,0 +1,10 @@
1
+ export default class VelociousDatabaseDriversPgsqlColumn {
2
+ constructor(table, data) {
3
+ this.data = data
4
+ this.table = table
5
+ }
6
+
7
+ getName() {
8
+ return this.data.column_name
9
+ }
10
+ }
@@ -0,0 +1,13 @@
1
+ import {digg} from "diggerize"
2
+
3
+ export default class VelociousDatabaseDriversPgsqlForeignKey {
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", "foreign_column_name")
12
+ getReferencedTableName = () => digg(this, "data", "foreign_table_name")
13
+ }