velocious 1.0.32 → 1.0.34
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 +23 -12
- package/package.json +1 -1
- package/spec/database/record/translations-spec.js +10 -0
- package/spec/http-server/get-spec.js +14 -10
- package/spec/http-server/post-spec.js +51 -45
- package/spec/http-server/root-get-spec.js +14 -10
- package/src/database/record/index.js +6 -3
- package/src/http-server/client/index.js +1 -4
- package/src/http-server/client/request-buffer/index.js +13 -6
- package/src/http-server/client/request-parser.js +8 -0
- package/src/http-server/server-client.js +7 -3
- package/src/routes/resolver.js +3 -2
- package/src/testing/request-client.js +9 -2
package/README.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# README
|
|
2
2
|
|
|
3
|
-
This is still work in progress.
|
|
4
|
-
|
|
5
3
|
* Concurrent multi threadded web server
|
|
6
4
|
* Database framework ala Rails
|
|
7
5
|
* Database models ala Rails
|
|
@@ -19,23 +17,29 @@ npm install velocious
|
|
|
19
17
|
npx velocious init
|
|
20
18
|
```
|
|
21
19
|
|
|
22
|
-
#
|
|
20
|
+
# Models
|
|
23
21
|
|
|
24
22
|
```bash
|
|
25
|
-
npx velocious g:
|
|
23
|
+
npx velocious g:model Account
|
|
24
|
+
npx velocious g:model Task
|
|
26
25
|
```
|
|
27
26
|
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
```
|
|
27
|
+
```js
|
|
28
|
+
import Record from "velocious/src/database/record/index.js"
|
|
31
29
|
|
|
32
|
-
|
|
30
|
+
class Task extends Record {
|
|
31
|
+
}
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
Task.belongsTo("account")
|
|
34
|
+
Task.translates("description", "subTitle", "title")
|
|
35
|
+
|
|
36
|
+
export default Task
|
|
36
37
|
```
|
|
37
38
|
|
|
38
39
|
# Migrations
|
|
40
|
+
|
|
41
|
+
Make a new migration from a template like this:
|
|
42
|
+
|
|
39
43
|
```bash
|
|
40
44
|
npx velocious g:migration create-tasks
|
|
41
45
|
```
|
|
@@ -66,7 +70,12 @@ export default class CreateEvents extends Migration {
|
|
|
66
70
|
}
|
|
67
71
|
```
|
|
68
72
|
|
|
69
|
-
Run migrations from
|
|
73
|
+
Run migrations from the command line like this:
|
|
74
|
+
```bash
|
|
75
|
+
npx velocious db:migrate
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Run migrations from anywhere if you want to:
|
|
70
79
|
|
|
71
80
|
```js
|
|
72
81
|
const migrationsPath = `/some/dir/migrations`
|
|
@@ -88,6 +97,8 @@ import {Task} from "@/src/models/task"
|
|
|
88
97
|
const tasks = await Task
|
|
89
98
|
.preload({project: {account: true}})
|
|
90
99
|
.where({projects: {public: true}})
|
|
100
|
+
.order("name")
|
|
101
|
+
.limit(5)
|
|
91
102
|
.toArray()
|
|
92
103
|
```
|
|
93
104
|
|
|
@@ -104,7 +115,7 @@ If you are developing on Velocious, you can run the tests with:
|
|
|
104
115
|
npm run test
|
|
105
116
|
```
|
|
106
117
|
|
|
107
|
-
# Writing a
|
|
118
|
+
# Writing a request test
|
|
108
119
|
|
|
109
120
|
First create a test file under something like the following path 'src/routes/accounts/create-test.js' with something like the following content:
|
|
110
121
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import Record from "../../../src/database/record/index.js"
|
|
2
|
+
|
|
3
|
+
class EventSeries extends Record {
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
describe("Record - translations", () => {
|
|
7
|
+
it("handles difficult table names", async () => {
|
|
8
|
+
expect(EventSeries.getTranslationsTableName()).toEqual("event_series_translations")
|
|
9
|
+
})
|
|
10
|
+
})
|
|
@@ -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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
58
|
+
for (let i = 0; i <= 5; i++) {
|
|
59
|
+
const body = new FormData()
|
|
55
60
|
|
|
56
|
-
|
|
61
|
+
body.append("project[name]", "Test create project")
|
|
57
62
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
})
|
|
@@ -395,10 +395,9 @@ export default class VelociousDatabaseRecord {
|
|
|
395
395
|
const className = `${this.name}Translation`
|
|
396
396
|
const TranslationClass = class Translation extends VelociousDatabaseRecord {}
|
|
397
397
|
const belongsTo = `${inflection.camelize(inflection.singularize(this.tableName()), true)}`
|
|
398
|
-
const tableName = `${inflection.singularize(this.tableName())}_translations`
|
|
399
398
|
|
|
400
399
|
Object.defineProperty(TranslationClass, "name", {value: className})
|
|
401
|
-
TranslationClass.setTableName(
|
|
400
|
+
TranslationClass.setTableName(this.getTranslationsTableName())
|
|
402
401
|
TranslationClass.belongsTo(belongsTo)
|
|
403
402
|
|
|
404
403
|
this._translationClass = TranslationClass
|
|
@@ -407,7 +406,11 @@ export default class VelociousDatabaseRecord {
|
|
|
407
406
|
}
|
|
408
407
|
|
|
409
408
|
static getTranslationsTableName() {
|
|
410
|
-
|
|
409
|
+
const tableNameParts = this.tableName().split("_")
|
|
410
|
+
|
|
411
|
+
tableNameParts[tableNameParts.length - 1] = inflection.singularize(tableNameParts[tableNameParts.length - 1])
|
|
412
|
+
|
|
413
|
+
return `${tableNameParts.join("_")}_translations`
|
|
411
414
|
}
|
|
412
415
|
|
|
413
416
|
static async hasTranslationsTable() {
|
|
@@ -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
|
|
78
|
-
this.postBodyChars.push(char)
|
|
75
|
+
this.postBodyChars[this.bodyLength - 1] = char
|
|
79
76
|
|
|
80
|
-
if (this.contentLength && this.
|
|
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
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
package/src/routes/resolver.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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) {
|