velocious 1.0.31 → 1.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -66,6 +66,20 @@ export default class CreateEvents extends Migration {
66
66
  }
67
67
  ```
68
68
 
69
+ Run migrations from anywhere
70
+
71
+ ```js
72
+ const migrationsPath = `/some/dir/migrations`
73
+ const files = await new FilesFinder({path: migrationsPath}).findFiles()
74
+
75
+ await this.configuration.withConnections(async () => {
76
+ const migrator = new Migrator({configuration: this.configuration})
77
+
78
+ await migrator.prepare()
79
+ await migrator.migrateFiles(files, async (path) => await import(path))
80
+ })
81
+ ```
82
+
69
83
  # Querying
70
84
 
71
85
  ```js
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "velocious": "bin/velocious.js"
4
4
  },
5
5
  "name": "velocious",
6
- "version": "1.0.31",
6
+ "version": "1.0.33",
7
7
  "main": "index.js",
8
8
  "scripts": {
9
9
  "test": "jasmine",
@@ -4,23 +4,27 @@ import Dummy from "../dummy/index.js"
4
4
  describe("HttpServer", () => {
5
5
  it("handles get requests", async () => {
6
6
  await Dummy.run(async () => {
7
- const response = await fetch("http://localhost:3006/tasks")
8
- const text = await response.text()
7
+ for (let i = 0; i <= 5; i++) {
8
+ const response = await fetch("http://localhost:3006/tasks")
9
+ const text = await response.text()
9
10
 
10
- expect(response.status).toEqual(200)
11
- expect(response.statusText).toEqual("OK")
12
- expect(text).toEqual("1, 2, 3, 4, 5\n")
11
+ expect(response.status).toEqual(200)
12
+ expect(response.statusText).toEqual("OK")
13
+ expect(text).toEqual("1, 2, 3, 4, 5\n")
14
+ }
13
15
  })
14
16
  })
15
17
 
16
18
  it("returns a 404 error when a collection action isnt found", async () => {
17
19
  await Dummy.run(async () => {
18
- const response = await fetch("http://localhost:3006/tasks/doesnt-exist")
19
- const text = await response.text()
20
+ for (let i = 0; i <= 5; i++) {
21
+ const response = await fetch("http://localhost:3006/tasks/doesnt-exist")
22
+ const text = await response.text()
20
23
 
21
- expect(response.status).toEqual(404)
22
- expect(response.statusText).toEqual("Not Found")
23
- expect(text).toEqual("Not found!\n")
24
+ expect(response.status).toEqual(404)
25
+ expect(response.statusText).toEqual("Not Found")
26
+ expect(text).toEqual("Not found!\n")
27
+ }
24
28
  })
25
29
  })
26
30
  })
@@ -7,66 +7,72 @@ import Project from "../dummy/src/models/project.js"
7
7
  describe("HttpServer", () => {
8
8
  it("handles post requests", async () => {
9
9
  await Dummy.run(async () => {
10
- const postData = querystring.stringify({"project[name]": "Test create project"})
11
- const response = await fetch(
12
- "http://localhost:3006/projects",
13
- {
14
- body: postData,
15
- headers: {
16
- "Content-Type": "application/x-www-form-urlencoded",
17
- "Content-Length": Buffer.byteLength(postData)
18
- },
19
- method: "POST"
20
- }
21
- )
22
- const text = await response.text()
23
- const createdProject = await Project.preload({translations: true}).last()
10
+ for (let i = 0; i <= 5; i++) {
11
+ const postData = querystring.stringify({"project[name]": "Test create project"})
12
+ const response = await fetch(
13
+ "http://localhost:3006/projects",
14
+ {
15
+ body: postData,
16
+ headers: {
17
+ "Content-Type": "application/x-www-form-urlencoded",
18
+ "Content-Length": Buffer.byteLength(postData)
19
+ },
20
+ method: "POST"
21
+ }
22
+ )
23
+ const text = await response.text()
24
+ const createdProject = await Project.preload({translations: true}).last()
24
25
 
25
- expect(text).toEqual('{"status":"success"}')
26
- expect(createdProject.name()).toEqual("Test create project")
26
+ expect(text).toEqual('{"status":"success"}')
27
+ expect(createdProject.name()).toEqual("Test create project")
28
+ }
27
29
  })
28
30
  })
29
31
 
30
32
  it("handles post json requests", async () => {
31
33
  await Dummy.run(async () => {
32
- const postData = JSON.stringify({project: {name: "Test create project"}})
33
- const response = await fetch(
34
- "http://localhost:3006/projects",
35
- {
36
- body: postData,
37
- headers: {
38
- "Content-Type": "application/json",
39
- "Content-Length": Buffer.byteLength(postData)
40
- },
41
- method: "POST"
42
- }
43
- )
44
- const text = await response.text()
45
- const createdProject = await Project.preload({translations: true}).last()
34
+ for (let i = 0; i <= 5; i++) {
35
+ const postData = JSON.stringify({project: {name: "Test create project"}})
36
+ const response = await fetch(
37
+ "http://localhost:3006/projects",
38
+ {
39
+ body: postData,
40
+ headers: {
41
+ "Content-Type": "application/json",
42
+ "Content-Length": Buffer.byteLength(postData)
43
+ },
44
+ method: "POST"
45
+ }
46
+ )
47
+ const text = await response.text()
48
+ const createdProject = await Project.preload({translations: true}).last()
46
49
 
47
- expect(text).toEqual('{"status":"success"}')
48
- expect(createdProject.name()).toEqual("Test create project")
50
+ expect(text).toEqual('{"status":"success"}')
51
+ expect(createdProject.name()).toEqual("Test create project")
52
+ }
49
53
  })
50
54
  })
51
55
 
52
56
  it("handles post form-data requests", async () => {
53
57
  await Dummy.run(async () => {
54
- const body = new FormData()
58
+ for (let i = 0; i <= 5; i++) {
59
+ const body = new FormData()
55
60
 
56
- body.append("project[name]", "Test create project")
61
+ body.append("project[name]", "Test create project")
57
62
 
58
- const response = await fetch(
59
- "http://localhost:3006/projects",
60
- {
61
- body,
62
- method: "POST"
63
- }
64
- )
65
- const text = await response.text()
66
- const createdProject = await Project.preload({translations: true}).last()
63
+ const response = await fetch(
64
+ "http://localhost:3006/projects",
65
+ {
66
+ body,
67
+ method: "POST"
68
+ }
69
+ )
70
+ const text = await response.text()
71
+ const createdProject = await Project.preload({translations: true}).last()
67
72
 
68
- expect(text).toEqual('{"status":"success"}')
69
- expect(createdProject.name()).toEqual("Test create project")
73
+ expect(text).toEqual('{"status":"success"}')
74
+ expect(createdProject.name()).toEqual("Test create project")
75
+ }
70
76
  })
71
77
  })
72
78
  })
@@ -4,23 +4,27 @@ import Dummy from "../dummy/index.js"
4
4
  describe("HttpServer", () => {
5
5
  it("handles root get requests", async () => {
6
6
  await Dummy.run(async () => {
7
- const response = await fetch("http://localhost:3006/ping")
8
- const text = await response.text()
7
+ for (let i = 0; i <= 5; i++) {
8
+ const response = await fetch("http://localhost:3006/ping")
9
+ const text = await response.text()
9
10
 
10
- expect(response.status).toEqual(200)
11
- expect(response.statusText).toEqual("OK")
12
- expect(text).toEqual("{\"message\":\"Pong\"}")
11
+ expect(response.status).toEqual(200)
12
+ expect(response.statusText).toEqual("OK")
13
+ expect(text).toEqual("{\"message\":\"Pong\"}")
14
+ }
13
15
  })
14
16
  })
15
17
 
16
18
  it("returns a 404 error when a root get isn't found", async () => {
17
19
  await Dummy.run(async () => {
18
- const response = await fetch("http://localhost:3006/doesnt-exist")
19
- const text = await response.text()
20
+ for (let i = 0; i <= 5; i++) {
21
+ const response = await fetch("http://localhost:3006/doesnt-exist")
22
+ const text = await response.text()
20
23
 
21
- expect(response.status).toEqual(404)
22
- expect(response.statusText).toEqual("Not Found")
23
- expect(text).toEqual("Not found!\n")
24
+ expect(response.status).toEqual(404)
25
+ expect(response.statusText).toEqual("Not Found")
26
+ expect(text).toEqual("Not found!\n")
27
+ }
24
28
  })
25
29
  })
26
30
  })
@@ -1,33 +1,13 @@
1
1
  import BaseCommand from "../../base-command.js"
2
- import fs from "node:fs/promises"
3
- import * as inflection from "inflection"
2
+ import FilesFinder from "../../../database/migrator/files-finder.js"
4
3
  import Migrator from "../../../database/migrator.js"
5
4
 
6
5
  export default class DbMigrate extends BaseCommand {
7
6
  async execute() {
8
7
  const projectPath = this.configuration.getDirectory()
9
8
  const migrationsPath = `${projectPath}/src/database/migrations`
10
- let files = await fs.readdir(migrationsPath)
11
-
12
- files = files
13
- .map((file) => {
14
- const match = file.match(/^(\d{14})-(.+)\.js$/)
15
-
16
- if (!match) return null
17
-
18
- const date = parseInt(match[1])
19
- const migrationName = match[2]
20
- const migrationClassName = inflection.camelize(migrationName.replaceAll("-", "_"))
21
-
22
- return {
23
- file,
24
- fullPath: `${migrationsPath}/${file}`,
25
- date,
26
- migrationClassName
27
- }
28
- })
29
- .filter((migration) => Boolean(migration))
30
- .sort((migration1, migration2) => migration1.date - migration2.date)
9
+ const filesFinder = new FilesFinder({path: migrationsPath})
10
+ const files = await filesFinder.findFiles()
31
11
 
32
12
  this.migrator = new Migrator({configuration: this.configuration})
33
13
 
@@ -1,33 +1,13 @@
1
1
  import BaseCommand from "../../base-command.js"
2
- import fs from "node:fs/promises"
3
- import * as inflection from "inflection"
2
+ import FilesFinder from "../../../database/migrator/files-finder.js"
4
3
  import Migrator from "../../../database/migrator.js"
5
4
 
6
5
  export default class DbReset extends BaseCommand {
7
6
  async execute() {
8
7
  const projectPath = this.configuration.getDirectory()
9
8
  const migrationsPath = `${projectPath}/src/database/migrations`
10
- let files = await fs.readdir(migrationsPath)
11
-
12
- files = files
13
- .map((file) => {
14
- const match = file.match(/^(\d{14})-(.+)\.js$/)
15
-
16
- if (!match) return null
17
-
18
- const date = parseInt(match[1])
19
- const migrationName = match[2]
20
- const migrationClassName = inflection.camelize(migrationName.replaceAll("-", "_"))
21
-
22
- return {
23
- file,
24
- fullPath: `${migrationsPath}/${file}`,
25
- date,
26
- migrationClassName
27
- }
28
- })
29
- .filter((migration) => Boolean(migration))
30
- .sort((migration1, migration2) => migration1.date - migration2.date)
9
+ const filesFinder = new FilesFinder({path: migrationsPath})
10
+ const files = await filesFinder.findFiles()
31
11
 
32
12
  this.migrator = new Migrator({configuration: this.configuration})
33
13
 
@@ -0,0 +1,40 @@
1
+ import fs from "node:fs/promises"
2
+ import * as inflection from "inflection"
3
+
4
+ import restArgsError from "../../utils/rest-args-error.js"
5
+
6
+ export default class VelociousDatabaseMigratorFilesFinder {
7
+ constructor({path, ...restArgs}) {
8
+ restArgsError(restArgs)
9
+
10
+ if (!path) throw new Error("No path given")
11
+
12
+ this.path = path
13
+ }
14
+
15
+ async findFiles() {
16
+ let files = await fs.readdir(this.path)
17
+
18
+ files = files
19
+ .map((file) => {
20
+ const match = file.match(/^(\d{14})-(.+)\.js$/)
21
+
22
+ if (!match) return null
23
+
24
+ const date = parseInt(match[1])
25
+ const migrationName = match[2]
26
+ const migrationClassName = inflection.camelize(migrationName.replaceAll("-", "_"))
27
+
28
+ return {
29
+ file,
30
+ fullPath: `${this.path}/${file}`,
31
+ date,
32
+ migrationClassName
33
+ }
34
+ })
35
+ .filter((migration) => Boolean(migration))
36
+ .sort((migration1, migration2) => migration1.date - migration2.date)
37
+
38
+ return files
39
+ }
40
+ }
@@ -177,6 +177,7 @@ export default class VelociousDatabaseMigrator {
177
177
  async runMigrationFile({migration, requireMigration}) {
178
178
  if (!this.configuration) throw new Error("No configuration set")
179
179
  if (!this.configuration.isDatabasePoolInitialized()) await this.configuration.initializeDatabasePool()
180
+ if (!this.migrationsVersions) await this.loadMigrationsVersions()
180
181
 
181
182
  const dbs = await this.configuration.getCurrentConnections()
182
183
  const migrationClass = await requireMigration()
@@ -42,6 +42,10 @@ export default class VelociousDatabaseQueryCreateTableBase extends QueryBase {
42
42
  maxlength ||= 255
43
43
  }
44
44
 
45
+ if (databaseType == "mssql" && type == "BOOLEAN") {
46
+ type = "BIT"
47
+ }
48
+
45
49
  if (databaseType == "sqlite" && column.getAutoIncrement() && column.getPrimaryKey()) {
46
50
  type = "INTEGER"
47
51
  }
@@ -362,7 +362,9 @@ export default class VelociousDatabaseRecord {
362
362
  }
363
363
 
364
364
  static tableName() {
365
- return inflection.underscore(inflection.pluralize(this.name))
365
+ if (!this._tableName) this._tableName = inflection.underscore(inflection.pluralize(this.name))
366
+
367
+ return this._tableName
366
368
  }
367
369
 
368
370
  static setTableName(tableName) {
@@ -38,10 +38,7 @@ export default class VeoliciousHttpServerClient {
38
38
 
39
39
  onWrite(data) {
40
40
  if (this.state == "initial") {
41
- this.currentRequest = new Request({
42
- configuration: this.configuration
43
- })
44
-
41
+ this.currentRequest = new Request({configuration: this.configuration})
45
42
  this.currentRequest.requestParser.events.on("done", this.executeCurrentRequest)
46
43
  this.currentRequest.feed(data)
47
44
  this.state = "requestStarted"
@@ -13,8 +13,6 @@ export default class RequestBuffer {
13
13
  headers = []
14
14
  headersByName = {}
15
15
  params = {}
16
- postBody = ""
17
- postBodyChars = []
18
16
  readingBody = false
19
17
  state = "status"
20
18
 
@@ -74,10 +72,9 @@ export default class RequestBuffer {
74
72
 
75
73
  break
76
74
  case "post-body":
77
- this.bodyLength += 1
78
- this.postBodyChars.push(char)
75
+ this.postBodyChars[this.bodyLength - 1] = char
79
76
 
80
- if (this.contentLength && this.postBodyChars.length >= this.contentLength) {
77
+ if (this.contentLength && this.bodyLength >= this.contentLength) {
81
78
  this.postRequestDone()
82
79
  }
83
80
 
@@ -157,6 +154,7 @@ export default class RequestBuffer {
157
154
  this.completeRequest()
158
155
  } else if (this.httpMethod.toUpperCase() == "POST") {
159
156
  this.readingBody = true
157
+ this.bodyLength = 0
160
158
 
161
159
  const match = this.getHeader("content-type")?.value?.match(/^multipart\/form-data;\s*boundary=(.+)$/i)
162
160
 
@@ -168,6 +166,11 @@ export default class RequestBuffer {
168
166
  this.multiPartyFormData = true
169
167
  this.setState("multi-part-form-data")
170
168
  } else {
169
+ if (!this.contentLength) throw new Error("Content length hasn't been set")
170
+
171
+ this.postBodyBuffer = new ArrayBuffer(this.contentLength)
172
+ this.postBodyChars = new Uint8Array(this.postBodyBuffer)
173
+
171
174
  this.setState("post-body")
172
175
  }
173
176
  } else {
@@ -190,7 +193,11 @@ export default class RequestBuffer {
190
193
  }
191
194
 
192
195
  postRequestDone() {
193
- this.postBody += String.fromCharCode.apply(null, this.postBodyChars)
196
+ this.postBody = String.fromCharCode.apply(null, this.postBodyChars)
197
+
198
+ delete this.postBodyChars
199
+ delete this.postBodyBuffer
200
+
194
201
  this.parseQueryStringPostParams()
195
202
  this.completeRequest()
196
203
  }
@@ -19,6 +19,13 @@ export default class VelociousHttpServerClientRequestParser {
19
19
  this.requestBuffer.events.on("request-done", this.requestDone)
20
20
  }
21
21
 
22
+ destroy() {
23
+ this.requestBuffer.events.off("completed", this.requestDone)
24
+ this.requestBuffer.events.off("form-data-part", this.onFormDataPart)
25
+ this.requestBuffer.events.off("request-done", this.requestDone)
26
+ this.requestBuffer.destroy()
27
+ }
28
+
22
29
  onFormDataPart = (formDataPart) => {
23
30
  const unorderedParams = {}
24
31
 
@@ -78,6 +85,7 @@ export default class VelociousHttpServerClientRequestParser {
78
85
  const incorporator = new Incorporator({objects: [this.params, this.requestBuffer.params]})
79
86
 
80
87
  incorporator.merge()
88
+
81
89
  this.state = "done"
82
90
  this.events.emit("done")
83
91
  }
@@ -19,9 +19,13 @@ export default class ServerClient {
19
19
 
20
20
  close() {
21
21
  return new Promise((resolve, reject) => {
22
- this.socket.destroy()
23
- this.events.emit("close", this)
24
- resolve()
22
+ try {
23
+ this.socket.destroy()
24
+ this.events.emit("close", this)
25
+ resolve()
26
+ } catch (error) {
27
+ reject(error)
28
+ }
25
29
  })
26
30
  }
27
31
 
@@ -1,7 +1,8 @@
1
1
  import {digg, digs} from "diggerize"
2
+ import {dirname} from "path"
2
3
  import {fileURLToPath} from "url"
3
4
  import fs from "fs/promises"
4
- import {dirname} from "path"
5
+ import * as inflection from "inflection"
5
6
 
6
7
  export default class VelociousRoutesResolver {
7
8
  constructor({configuration, request, response}) {
@@ -22,7 +23,7 @@ export default class VelociousRoutesResolver {
22
23
  let viewPath
23
24
 
24
25
  const matchResult = this.matchPathWithRoutes(currentRoute, currentPath)
25
- let action = this.params.action
26
+ let action = this.params.action ? inflection.camelize(this.params.action.replaceAll("-", "_"), true) : undefined
26
27
  let controller = this.params.controller
27
28
 
28
29
  if (!matchResult) {
@@ -5,6 +5,8 @@ class Response {
5
5
 
6
6
  async parse() {
7
7
  this._body = await this.fetchResponse.text()
8
+
9
+ if (this.statusCode() != 200) throw new Error(`Request failed with code ${this.statusCode()} and body: ${this.body()}`)
8
10
  }
9
11
 
10
12
  body = () => this._body
@@ -16,8 +18,13 @@ export default class RequestClient {
16
18
  host = "localhost"
17
19
  port = 31006
18
20
 
19
- get() {
20
- throw new Error("get stub")
21
+ async get(path) {
22
+ const fetchResponse = await fetch(`http://${this.host}:${this.port}${path}`)
23
+ const response = new Response(fetchResponse)
24
+
25
+ await response.parse()
26
+
27
+ return response
21
28
  }
22
29
 
23
30
  async post(path, data) {