velocious 1.0.1 → 1.0.3

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 (157) hide show
  1. package/README.md +36 -0
  2. package/bin/velocious.mjs +8 -0
  3. package/index.mjs +21 -0
  4. package/package.json +15 -7
  5. package/peak_flow.yml +12 -5
  6. package/spec/cli/commands/db/create-spec.mjs +25 -0
  7. package/spec/cli/commands/db/migrate-spec.mjs +37 -0
  8. package/spec/cli/commands/destroy/migration-spec.mjs +15 -0
  9. package/spec/cli/commands/generate/migration-spec.mjs +18 -0
  10. package/spec/cli/commands/init-spec.mjs +19 -0
  11. package/spec/cli/commands/test/test-files-finder-spec.mjs +12 -0
  12. package/spec/database/connection/drivers/mysql/{query-parser-spec.cjs → query-parser-spec.mjs} +12 -8
  13. package/spec/database/drivers/mysql/connection-spec.mjs +21 -0
  14. package/spec/database/record/create-spec.mjs +14 -0
  15. package/spec/database/record/destroy-spec.mjs +17 -0
  16. package/spec/database/record/find-spec.mjs +29 -0
  17. package/spec/database/record/update-spec.mjs +15 -0
  18. package/spec/dummy/dummy-directory.mjs +11 -0
  19. package/spec/dummy/index.mjs +63 -0
  20. package/spec/dummy/src/config/configuration.example.mjs +19 -0
  21. package/spec/dummy/src/config/configuration.peakflow.mjs +20 -0
  22. package/spec/dummy/src/{routes.cjs → config/routes.mjs} +3 -2
  23. package/spec/dummy/src/database/migrations/20230728075328-create-projects.mjs +11 -0
  24. package/spec/dummy/src/database/migrations/20230728075329-create-tasks.mjs +13 -0
  25. package/spec/dummy/src/models/task.mjs +4 -0
  26. package/spec/dummy/src/routes/tasks/controller.mjs +26 -0
  27. package/spec/http-server/{client-spec.cjs → client-spec.mjs} +7 -10
  28. package/spec/http-server/{get-spec.cjs → get-spec.mjs} +2 -2
  29. package/spec/http-server/post-spec.mjs +72 -0
  30. package/spec/support/jasmine.json +4 -3
  31. package/src/application.mjs +50 -0
  32. package/src/cli/base-command.mjs +11 -0
  33. package/src/cli/commands/db/create.mjs +50 -0
  34. package/src/cli/commands/db/migrate.mjs +58 -0
  35. package/src/cli/commands/destroy/migration.mjs +35 -0
  36. package/src/cli/commands/generate/migration.mjs +36 -0
  37. package/src/cli/commands/init.mjs +60 -0
  38. package/src/cli/commands/test/index.mjs +14 -0
  39. package/src/cli/commands/test/test-files-finder.mjs +99 -0
  40. package/src/cli/commands/test/test-runner.mjs +19 -0
  41. package/src/cli/index.mjs +59 -0
  42. package/src/configuration-resolver.mjs +26 -0
  43. package/src/configuration.mjs +49 -0
  44. package/src/{controller.cjs → controller.mjs} +21 -4
  45. package/src/database/drivers/base.mjs +17 -0
  46. package/src/database/drivers/index.mjs +5 -0
  47. package/src/database/drivers/mysql/connect-connection.mjs +12 -0
  48. package/src/database/drivers/mysql/index.mjs +102 -0
  49. package/src/database/drivers/mysql/options.mjs +17 -0
  50. package/src/database/drivers/mysql/query-parser.mjs +25 -0
  51. package/src/database/drivers/mysql/query.mjs +26 -0
  52. package/src/database/drivers/mysql/sql/create-database.mjs +4 -0
  53. package/src/database/drivers/mysql/sql/create-table.mjs +4 -0
  54. package/src/database/drivers/mysql/sql/delete.mjs +19 -0
  55. package/src/database/drivers/mysql/sql/insert.mjs +29 -0
  56. package/src/database/drivers/mysql/sql/update.mjs +31 -0
  57. package/src/database/drivers/sqlite/options.mjs +17 -0
  58. package/src/database/drivers/sqlite/query-parser.mjs +25 -0
  59. package/src/database/drivers/sqlite/sql/create-database.mjs +4 -0
  60. package/src/database/drivers/sqlite/sql/create-table.mjs +4 -0
  61. package/src/database/drivers/sqlite/sql/delete.mjs +19 -0
  62. package/src/database/drivers/sqlite/sql/insert.mjs +29 -0
  63. package/src/database/drivers/sqlite/sql/update.mjs +31 -0
  64. package/src/database/drivers/sqlite-expo/index.mjs +100 -0
  65. package/src/database/drivers/sqlite-expo/query.mjs +9 -0
  66. package/src/database/handler.mjs +7 -0
  67. package/src/database/index.mjs +15 -0
  68. package/src/database/migration/index.mjs +18 -0
  69. package/src/database/migrator/index.mjs +15 -0
  70. package/src/database/pool/index.mjs +112 -0
  71. package/src/database/query/base.mjs +11 -0
  72. package/src/database/query/create-database-base.mjs +20 -0
  73. package/src/database/query/create-table-base.mjs +69 -0
  74. package/src/database/query/delete-base.mjs +9 -0
  75. package/src/database/query/from-base.mjs +9 -0
  76. package/src/database/query/from-plain.mjs +10 -0
  77. package/src/database/query/from-table.mjs +12 -0
  78. package/src/database/query/index.mjs +144 -0
  79. package/src/database/query/insert-base.mjs +15 -0
  80. package/src/database/query/join-base.mjs +9 -0
  81. package/src/database/query/join-plain.mjs +12 -0
  82. package/src/database/query/order-base.mjs +9 -0
  83. package/src/database/query/order-plain.mjs +21 -0
  84. package/src/database/query/select-base.mjs +9 -0
  85. package/src/database/query/select-plain.mjs +12 -0
  86. package/src/database/query/{select-table-and-column.cjs → select-table-and-column.mjs} +4 -4
  87. package/src/database/query/update-base.mjs +16 -0
  88. package/src/database/query-parser/{from-parser.cjs → from-parser.mjs} +3 -6
  89. package/src/database/query-parser/{joins-parser.cjs → joins-parser.mjs} +3 -6
  90. package/src/database/query-parser/{options.cjs → options.mjs} +13 -2
  91. package/src/database/query-parser/{select-parser.cjs → select-parser.mjs} +7 -6
  92. package/src/database/record/index.mjs +187 -0
  93. package/src/database/record/record-not-found-error.mjs +1 -0
  94. package/src/database/table-data/index.mjs +83 -0
  95. package/src/{error-logger.js → error-logger.mjs} +1 -1
  96. package/src/http-server/client/{index.cjs → index.mjs} +10 -11
  97. package/src/http-server/client/params-to-object.mjs +68 -0
  98. package/src/http-server/client/request-buffer/form-data-part.mjs +42 -0
  99. package/src/http-server/client/request-buffer/header.mjs +7 -0
  100. package/src/http-server/client/request-buffer/index.mjs +229 -0
  101. package/src/http-server/client/request-parser.mjs +47 -0
  102. package/src/http-server/client/{request-runner.cjs → request-runner.mjs} +5 -5
  103. package/src/http-server/client/request.mjs +15 -0
  104. package/src/http-server/client/{response.cjs → response.mjs} +1 -1
  105. package/src/http-server/index.mjs +137 -0
  106. package/src/http-server/server-client.mjs +47 -0
  107. package/src/http-server/worker-handler/index.mjs +79 -0
  108. package/src/http-server/worker-handler/worker-script.mjs +4 -0
  109. package/src/http-server/worker-handler/worker-thread.mjs +65 -0
  110. package/src/{logger.cjs → logger.mjs} +2 -2
  111. package/src/routes/base-route.mjs +34 -0
  112. package/src/routes/{get-route.cjs → get-route.mjs} +5 -3
  113. package/src/routes/index.mjs +9 -0
  114. package/src/routes/{resolver.cjs → resolver.mjs} +17 -9
  115. package/src/routes/{resource-route.cjs → resource-route.mjs} +9 -5
  116. package/src/routes/root-route.mjs +6 -0
  117. package/src/spec/index.mjs +5 -0
  118. package/src/templates/configuration.mjs +17 -0
  119. package/src/templates/generate-migration.mjs +11 -0
  120. package/src/templates/routes.mjs +11 -0
  121. package/src/utils/file-exists.mjs +13 -0
  122. package/bin/velocious +0 -14
  123. package/index.cjs +0 -13
  124. package/spec/dummy/config/databases.example.json +0 -10
  125. package/spec/dummy/config/databases.json +0 -0
  126. package/spec/dummy/config/databases.peakflow.json +0 -11
  127. package/spec/dummy/index.cjs +0 -40
  128. package/spec/dummy/src/models/task.cjs +0 -4
  129. package/spec/dummy/src/routes/tasks/controller.cjs +0 -18
  130. package/src/application.cjs +0 -36
  131. package/src/configuration.cjs +0 -14
  132. package/src/database/connection/drivers/mysql/index.cjs +0 -5
  133. package/src/database/connection/drivers/mysql/options.cjs +0 -7
  134. package/src/database/connection/drivers/mysql/query-parser.cjs +0 -26
  135. package/src/database/connection/index.cjs +0 -2
  136. package/src/database/handler.cjs +0 -5
  137. package/src/database/index.cjs +0 -9
  138. package/src/database/pool/index.cjs +0 -2
  139. package/src/database/query/from-base.cjs +0 -15
  140. package/src/database/query/from-plain.cjs +0 -12
  141. package/src/database/query/from-table.cjs +0 -12
  142. package/src/database/query/index.cjs +0 -59
  143. package/src/database/query/join-base.cjs +0 -15
  144. package/src/database/query/join-plain.cjs +0 -12
  145. package/src/database/query/select-base.cjs +0 -15
  146. package/src/database/query/select-plain.cjs +0 -12
  147. package/src/database/record/index.cjs +0 -5
  148. package/src/http-server/client/request-parser.cjs +0 -92
  149. package/src/http-server/client/request.cjs +0 -25
  150. package/src/http-server/index.cjs +0 -78
  151. package/src/http-server/worker-handler/index.cjs +0 -78
  152. package/src/http-server/worker-handler/socket-handler.cjs +0 -35
  153. package/src/http-server/worker-handler/worker-script.cjs +0 -4
  154. package/src/http-server/worker-handler/worker-thread.cjs +0 -49
  155. package/src/routes/base-route.cjs +0 -25
  156. package/src/routes/index.cjs +0 -9
  157. package/src/routes/root-route.cjs +0 -4
@@ -0,0 +1,187 @@
1
+ import DatabasePool from "../pool/index.mjs"
2
+ import Handler from "../handler.mjs"
3
+ import inflection from "inflection"
4
+ import Query from "../query/index.mjs"
5
+ import RecordNotFoundError from "./record-not-found-error.mjs"
6
+
7
+ export default class VelociousDatabaseRecord {
8
+ static connection() {
9
+ const connection = DatabasePool.current().getCurrentConnection()
10
+
11
+ if (!connection) throw new Error("No connection?")
12
+
13
+ return connection
14
+ }
15
+
16
+ static async find(recordId) {
17
+ const conditions = {}
18
+
19
+ conditions[this.primaryKey()] = recordId
20
+
21
+ const record = await this.where(conditions).first()
22
+
23
+ if (!record) {
24
+ throw new RecordNotFoundError(`Couldn't find ${this.name} with '${this.primaryKey()}'=${recordId}`)
25
+ }
26
+
27
+ return record
28
+ }
29
+
30
+ static async last() {
31
+ const query = this._newQuery().order(this.primaryKey()).limit(1)
32
+ const record = await query.last()
33
+
34
+ return record
35
+ }
36
+
37
+ static primaryKey() {
38
+ return "id"
39
+ }
40
+
41
+ async save() {
42
+ if (this.isPersisted()) {
43
+ return await this._updateRecordWithChanges()
44
+ } else {
45
+ return await this._createNewRecord()
46
+ }
47
+ }
48
+
49
+ static tableName() {
50
+ return inflection.underscore(inflection.pluralize(this.name))
51
+ }
52
+
53
+ static _newQuery() {
54
+ const handler = new Handler()
55
+ const query = new Query({
56
+ driver: this.connection(),
57
+ handler,
58
+ modelClass: this
59
+ })
60
+
61
+ return query.from(this.tableName())
62
+ }
63
+
64
+ static orderableColumn() {
65
+ // Allow to change to 'created_at' if using UUID?
66
+
67
+ return this.primaryKey()
68
+ }
69
+
70
+ static where(object) {
71
+ const query = this._newQuery().where(object)
72
+
73
+ return query
74
+ }
75
+
76
+ constructor(attributes = {}) {
77
+ this._attributes = attributes
78
+ this._changes = {}
79
+ }
80
+
81
+ assign(attributesToAssign) {
82
+ for (const attributeToAssign in attributesToAssign) {
83
+ this._changes[attributeToAssign] = attributesToAssign[attributeToAssign]
84
+ }
85
+ }
86
+
87
+ attributes() {
88
+ return Object.assign({}, this._attributes, this._changes)
89
+ }
90
+
91
+ _connection() {
92
+ if (this.__connection) return this.__connection
93
+
94
+ return this.constructor.connection()
95
+ }
96
+
97
+ async destroy() {
98
+ const conditions = {}
99
+
100
+ conditions[this.constructor.primaryKey()] = this.id()
101
+
102
+ const sql = this._connection().deleteSql({
103
+ conditions,
104
+ tableName: this._tableName()
105
+ })
106
+
107
+ await this._connection().query(sql)
108
+ }
109
+
110
+ _tableName() {
111
+ if (this.__tableName) return this.__tableName
112
+
113
+ return this.constructor.tableName()
114
+ }
115
+
116
+ readAttribute(attributeName) {
117
+ if (attributeName in this._changes) return this._changes[attributeName]
118
+
119
+ return this._attributes[attributeName]
120
+ }
121
+
122
+ async _createNewRecord() {
123
+ if (!this.constructor.connection()["insertSql"]) {
124
+ throw new Error(`No insertSql on ${this.constructor.connection().constructor.name}`)
125
+ }
126
+
127
+ const sql = this._connection().insertSql({
128
+ tableName: this._tableName(),
129
+ data: this.attributes()
130
+ })
131
+ const result = await this._connection().query(sql)
132
+ const id = result.insertId
133
+
134
+ await this._reloadWithId(id)
135
+ }
136
+
137
+ async _updateRecordWithChanges() {
138
+ const conditions = {}
139
+
140
+ conditions[this.constructor.primaryKey()] = this.id()
141
+
142
+ const sql = this._connection().updateSql({
143
+ tableName: this._tableName(),
144
+ data: this._changes,
145
+ conditions
146
+ })
147
+ await this._connection().query(sql)
148
+ await this._reloadWithId(this.id())
149
+ }
150
+
151
+ id() {
152
+ return this.readAttribute(this.constructor.primaryKey())
153
+ }
154
+
155
+ isPersisted() {
156
+ if (this.id()) return true
157
+
158
+ return false
159
+ }
160
+
161
+ isNewRecord() {
162
+ return !this.isPersisted()
163
+ }
164
+
165
+ async _reloadWithId(id) {
166
+ const primaryKey = this.constructor.primaryKey()
167
+ const whereObject = {}
168
+
169
+ whereObject[primaryKey] = id
170
+
171
+ const query = this.constructor.where(whereObject)
172
+ const reloadedModel = await query.first()
173
+
174
+ this._attributes = reloadedModel.attributes()
175
+ this._changes = {}
176
+ }
177
+
178
+ async reload() {
179
+ this._reloadWithId(this.readAttribute("id"))
180
+ }
181
+
182
+ async update(attributesToAssign) {
183
+ if (attributesToAssign) this.assign(attributesToAssign)
184
+
185
+ await this.save()
186
+ }
187
+ }
@@ -0,0 +1 @@
1
+ export default class RecordNotFoundError extends Error {}
@@ -0,0 +1,83 @@
1
+ class TableColumn {
2
+ constructor(name, args) {
3
+ this.args = args
4
+ this.name = name
5
+ }
6
+ }
7
+
8
+ class TableIndex {
9
+ constructor(columns, args) {
10
+ this.args = args
11
+ this.columns = columns
12
+ }
13
+
14
+ getColumns = () => this.columns
15
+ getName = () => this.args.name
16
+ getUnique = () => Boolean(this.args.unique)
17
+ }
18
+
19
+ class TableReference {
20
+ constructor(name, args) {
21
+ this.args = args
22
+ this.name = name
23
+ }
24
+ }
25
+
26
+ export default class TableData {
27
+ _columns = []
28
+ _indexes = []
29
+ _references = []
30
+
31
+ constructor(name, args = {}) {
32
+ this.args = args
33
+ this._name = name
34
+ }
35
+
36
+ getColumns = () => this._columns
37
+ getName = () => this._name
38
+ getIfNotExists = () => this.args.ifNotExists
39
+ getIndexes = () => this._indexes
40
+ getReferences = () => this._references
41
+
42
+ bigint(name, args = {}) {
43
+ const columnArgs = Object.assign({type: "bigint"}, args)
44
+ const column = new TableColumn(name, columnArgs)
45
+
46
+ this._columns.push(column)
47
+ }
48
+
49
+ references(name, args = {}) {
50
+ const columnName = `${name}_id`
51
+ const indexName = `index_on_${columnName}`
52
+ const reference = new TableReference(name, args)
53
+ const columnArgs = Object.assign({type: "bigint"}, args)
54
+ const column = new TableColumn(columnName, columnArgs)
55
+ const index = new TableIndex([column], {name: indexName})
56
+
57
+ this._columns.push(column)
58
+ this._indexes.push(index)
59
+ this._references.push(reference)
60
+ }
61
+
62
+ string(name, args) {
63
+ const columnArgs = Object.assign({type: "string"}, args)
64
+ const column = new TableColumn(name, columnArgs)
65
+
66
+ this._columns.push(column)
67
+ }
68
+
69
+ text(name, args) {
70
+ const columnArgs = Object.assign({type: "text"}, args)
71
+ const column = new TableColumn(name, columnArgs)
72
+
73
+ this._columns.push(column)
74
+ }
75
+
76
+ timestamps() {
77
+ const createdAtColumn = new TableColumn("created_at", {type: "datetime"})
78
+ const updatedAtColumn = new TableColumn("updated_at", {type: "datetime"})
79
+
80
+ this._columns.push(createdAtColumn)
81
+ this._columns.push(updatedAtColumn)
82
+ }
83
+ }
@@ -1,4 +1,4 @@
1
- module.exports = function errorLogger(callback) {
1
+ export default function errorLogger(callback) {
2
2
  return async function(...args) {
3
3
  try {
4
4
  await callback(...args)
@@ -1,11 +1,10 @@
1
- const Configuration = require("../../configuration.cjs")
2
- const {digg} = require("diggerize")
3
- const {EventEmitter} = require("events")
4
- const logger = require("../../logger.cjs")
5
- const Request = require("./request.cjs")
6
- const RequestRunner = require("./request-runner.cjs")
1
+ import {digg} from "diggerize"
2
+ import {EventEmitter} from "events"
3
+ import logger from "../../logger.mjs"
4
+ import Request from "./request.mjs"
5
+ import RequestRunner from "./request-runner.mjs"
7
6
 
8
- module.exports = class VeoliciousHttpServerClient {
7
+ export default class VeoliciousHttpServerClient {
9
8
  events = new EventEmitter()
10
9
  state = "initial"
11
10
 
@@ -17,7 +16,7 @@ module.exports = class VeoliciousHttpServerClient {
17
16
  this.onExecuteRequest = onExecuteRequest
18
17
  }
19
18
 
20
- executeCurrentRequest() {
19
+ executeCurrentRequest = () => {
21
20
  logger(this, "executeCurrentRequest")
22
21
 
23
22
  this.state = "response"
@@ -28,7 +27,7 @@ module.exports = class VeoliciousHttpServerClient {
28
27
  routes: this.routes
29
28
  })
30
29
 
31
- requestRunner.events.on("done", (requestRunner) => this.sendResponse(requestRunner))
30
+ requestRunner.events.on("done", this.sendResponse)
32
31
  requestRunner.run()
33
32
  }
34
33
 
@@ -37,7 +36,7 @@ module.exports = class VeoliciousHttpServerClient {
37
36
  this.currentRequest = new Request({
38
37
  configuration: this.configuration
39
38
  })
40
- this.currentRequest.requestParser.events.on("done", () => this.executeCurrentRequest())
39
+ this.currentRequest.requestParser.events.on("done", this.executeCurrentRequest)
41
40
  this.currentRequest.feed(data)
42
41
  this.state = "requestStarted"
43
42
  } else if (this.state == "requestStarted") {
@@ -47,7 +46,7 @@ module.exports = class VeoliciousHttpServerClient {
47
46
  }
48
47
  }
49
48
 
50
- sendResponse(requestRunner) {
49
+ sendResponse = (requestRunner) => {
51
50
  const response = digg(requestRunner, "response")
52
51
  const body = response.getBody()
53
52
  const date = new Date()
@@ -0,0 +1,68 @@
1
+ export default class ParamsToObject {
2
+ constructor(object) {
3
+ this.object = object
4
+ }
5
+
6
+ toObject() {
7
+ const result = {}
8
+
9
+ for(const key in this.object) {
10
+ const value = this.object[key]
11
+
12
+ this.treatInitial(key, value, result)
13
+ }
14
+
15
+ return result
16
+ }
17
+
18
+ treatInitial(key, value, result) {
19
+ const firstMatch = key.match(/^(.+?)(\[([\s\S]+$))/)
20
+
21
+ if (firstMatch) {
22
+ const inputName = firstMatch[1]
23
+ const rest = firstMatch[2]
24
+
25
+ let newResult
26
+
27
+ if (inputName in result) {
28
+ newResult = result[inputName]
29
+ } else if (rest == "[]") {
30
+ newResult = []
31
+ result[inputName] = newResult
32
+ } else {
33
+ newResult = {}
34
+ result[inputName] = newResult
35
+ }
36
+
37
+ this.treatSecond(value, rest, newResult)
38
+ } else {
39
+ result[key] = value
40
+ }
41
+ }
42
+
43
+ treatSecond(value, rest, result) {
44
+ const secondMatch = rest.match(/^\[(.*?)\]([\s\S]*)$/)
45
+ const key = secondMatch[1]
46
+ const newRest = secondMatch[2]
47
+
48
+ let newResult
49
+
50
+ if (rest == "[]") {
51
+ result.push(value)
52
+ } else if (newRest == "") {
53
+ result[key] = value
54
+ } else {
55
+ if (typeof result == "object" && key in result) {
56
+ newResult = result[key]
57
+ } else if (newRest == "[]") {
58
+ newResult = []
59
+ result[key] = newResult
60
+ } else {
61
+ newResult = {}
62
+ result[key] = newResult
63
+ }
64
+
65
+ this.treatSecond(value, newRest, newResult)
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,42 @@
1
+ export default class FormDataPart {
2
+ headers = {}
3
+ body = []
4
+
5
+ addHeader(header) {
6
+ const name = header.formattedName
7
+
8
+ this.headers[name] = header
9
+
10
+ if (name == "content-disposition") {
11
+ const match = header.value.match(/^form-data; name="(.+)"$/)
12
+
13
+ if (match) {
14
+ this.name = match[1]
15
+ } else {
16
+ console.error(`Couldn't match name from content-disposition: ${header.value}`)
17
+ }
18
+ } else if (name == "content-length") {
19
+ this.contentLength = parseInt(header.value)
20
+ }
21
+ }
22
+
23
+ finish() {
24
+ this.value = String.fromCharCode.apply(null, this.body)
25
+ }
26
+
27
+ getName() {
28
+ if (!this.name) throw new Error("Name hasn't been set")
29
+
30
+ return this.name
31
+ }
32
+
33
+ getValue() {
34
+ if (!this.value) throw new Error("Value hasn't been set")
35
+
36
+ return this.value
37
+ }
38
+
39
+ removeFromBody(text) {
40
+ this.body = this.body.slice(0, this.body.length - text.length)
41
+ }
42
+ }
@@ -0,0 +1,7 @@
1
+ export default class Header {
2
+ constructor(name, value) {
3
+ this.formattedName = name.toLowerCase().trim()
4
+ this.name = name
5
+ this.value = value
6
+ }
7
+ }
@@ -0,0 +1,229 @@
1
+ import {EventEmitter} from "events"
2
+ import FormDataPart from "./form-data-part.mjs"
3
+ import Header from "./header.mjs"
4
+ import Incorporator from "incorporator"
5
+ import logger from "../../../logger.mjs"
6
+ import ParamsToObject from "../params-to-object.mjs"
7
+ import querystring from "querystring"
8
+
9
+ export default class RequestBuffer {
10
+ bodyLength = 0
11
+ data = []
12
+ events = new EventEmitter()
13
+ headers = []
14
+ headersByName = {}
15
+ params = {}
16
+ postBody = ""
17
+ postBodyChars = []
18
+ readingBody = false
19
+ state = "status"
20
+
21
+ constructor({configuration}) {
22
+ this.configuration = configuration
23
+ }
24
+
25
+ feed(data) {
26
+ for (const char of data) {
27
+ if (this.readingBody) this.bodyLength += 1
28
+
29
+ switch(this.state) {
30
+ case "status":
31
+ case "headers":
32
+ case "multi-part-form-data":
33
+ case "multi-part-form-data-header":
34
+ this.data.push(char)
35
+
36
+ if (char == 10) {
37
+ const line = String.fromCharCode.apply(null, this.data)
38
+
39
+ this.data = []
40
+ this.parse(line)
41
+ }
42
+
43
+ break
44
+ case "multi-part-form-data-body":
45
+ const body = this.formDataPart.body
46
+
47
+ body.push(char)
48
+
49
+ const possibleBoundaryEndPosition = body.length - this.boundaryLineEnd.length
50
+ const possibleBoundaryEndChars = body.slice(possibleBoundaryEndPosition, body.length)
51
+ const possibleBoundaryEnd = String.fromCharCode.apply(null, possibleBoundaryEndChars)
52
+
53
+ const possibleBoundaryNextPosition = body.length - this.boundaryLineNext.length
54
+ const possibleBoundaryNextChars = body.slice(possibleBoundaryNextPosition, body.length)
55
+ const possibleBoundaryNext = String.fromCharCode.apply(null, possibleBoundaryNextChars)
56
+
57
+ if (possibleBoundaryEnd == this.boundaryLineEnd) {
58
+ this.formDataPart.removeFromBody(possibleBoundaryEnd)
59
+ this.formDataPartDone()
60
+ this.completeRequest()
61
+ } else if (possibleBoundaryNext == this.boundaryLineNext) {
62
+ this.formDataPart.removeFromBody(possibleBoundaryNext)
63
+ this.formDataPartDone()
64
+ this.newFormDataPart()
65
+ } else if (this.contentLength && this.bodyLength >= this.contentLength) {
66
+ this.formDataPartDone()
67
+ this.completeRequest()
68
+ } else if (this.formDataPart.contentLength && this.bodyLength >= this.formDataPart.contentLength) {
69
+ this.formDataPartDone()
70
+
71
+ throw new Error("stub")
72
+ }
73
+
74
+ break
75
+ case "post-body":
76
+ this.bodyLength += 1
77
+ this.postBodyChars.push(char)
78
+
79
+ if (this.contentLength && this.postBodyChars.length >= this.contentLength) {
80
+ this.postRequestDone()
81
+ }
82
+
83
+ break
84
+ default:
85
+ console.error(`Unknown state: ${this.state}`)
86
+ }
87
+ }
88
+ }
89
+
90
+ getHeader = (name) => this.headersByName[name.toLowerCase().trim()]
91
+
92
+ formDataPartDone() {
93
+ const formDataPart = this.formDataPart
94
+
95
+ this.formDataPart = undefined
96
+ formDataPart.finish()
97
+
98
+ this.events.emit("form-data-part", formDataPart)
99
+ }
100
+
101
+ isMultiPartyFormData = () => this.multiPartyFormData
102
+
103
+ newFormDataPart() {
104
+ this.formDataPart = new FormDataPart()
105
+ this.setState("multi-part-form-data-header")
106
+ }
107
+
108
+ parse(line) {
109
+ if (this.state == "status") {
110
+ this.parseStatusLine(line)
111
+ } else if (this.state == "headers") {
112
+ this.parseHeader(line)
113
+ } else if (this.state == "multi-part-form-data") {
114
+ if (line == this.boundaryLine) {
115
+ this.newFormDataPart()
116
+ } else if (line == "\r\n") {
117
+ this.setState("done")
118
+ } else {
119
+ throw new Error(`Expected boundary line but didn't get it: ${line}`)
120
+ }
121
+ } else if (this.state == "multi-part-form-data-header") {
122
+ const header = this.readHeaderFromLine(line)
123
+
124
+ if (header) {
125
+ this.formDataPart.addHeader(header)
126
+ this.state == "multi-part-form-data"
127
+ } else if (line == "\r\n") {
128
+ this.setState("multi-part-form-data-body")
129
+ }
130
+ } else {
131
+ throw new Error(`Unknown state: ${this.state}`)
132
+ }
133
+ }
134
+
135
+ readHeaderFromLine(line) {
136
+ let match
137
+
138
+ if (match = line.match(/^(.+): (.+)\r\n/)) {
139
+ const header = new Header(match[1], match[2])
140
+
141
+ return header
142
+ }
143
+ }
144
+
145
+ parseHeader(line) {
146
+ const header = this.readHeaderFromLine(line)
147
+
148
+ if (header) {
149
+ this.headersByName[header.formattedName] = header
150
+
151
+ if (header.formattedName == "content-length") this.contentLength = parseInt(header.value)
152
+
153
+ this.events.emit("header", header)
154
+ } else if (line == "\r\n") {
155
+ if (this.httpMethod.toUpperCase() == "GET") {
156
+ this.completeRequest()
157
+ } else if (this.httpMethod.toUpperCase() == "POST") {
158
+ this.readingBody = true
159
+
160
+ const match = this.getHeader("content-type").value.match(/^multipart\/form-data;\s*boundary=(.+)$/i)
161
+
162
+ if (match) {
163
+ this.boundary = match[1]
164
+ this.boundaryLine = `--${this.boundary}\r\n`
165
+ this.boundaryLineNext = `\r\n--${this.boundary}\r\n`
166
+ this.boundaryLineEnd = `\r\n--${this.boundary}--`
167
+ this.multiPartyFormData = true
168
+ this.setState("multi-part-form-data")
169
+ } else {
170
+ this.setState("post-body")
171
+ }
172
+ } else {
173
+ throw new Error(`Unknown HTTP method: ${this.httpMethod}`)
174
+ }
175
+ }
176
+ }
177
+
178
+ parseStatusLine(line) {
179
+ const match = line.match(/^(GET|POST) (.+?) HTTP\/1\.1\r\n/)
180
+
181
+ if (!match) {
182
+ throw new Error(`Couldn't match status line from: ${line}`)
183
+ }
184
+
185
+ this.httpMethod = match[1]
186
+ this.path = match[2]
187
+ this.setState("headers")
188
+ }
189
+
190
+ postRequestDone() {
191
+ this.postBody += String.fromCharCode.apply(null, this.postBodyChars)
192
+ this.parseQueryStringPostParams()
193
+ this.completeRequest()
194
+ }
195
+
196
+ setState(newState) {
197
+ logger(this, `Changing state from ${this.state} to ${newState}`)
198
+
199
+ this.state = newState
200
+ }
201
+
202
+ completeRequest = () => {
203
+ this.state = "completed"
204
+
205
+ if (this.getHeader("content-type")?.value?.startsWith("application/json")) {
206
+ this.parseApplicationJsonParams()
207
+ } else if (this.multiPartyFormData) {
208
+ // Done after each new form data part
209
+ }
210
+
211
+ this.events.emit("completed")
212
+ }
213
+
214
+ parseApplicationJsonParams() {
215
+ const newParams = JSON.parse(this.postBody)
216
+ const incorporator = new Incorporator({objects: [this.params, newParams]})
217
+
218
+ incorporator.merge()
219
+ }
220
+
221
+ parseQueryStringPostParams() {
222
+ const unparsedParams = querystring.parse(this.postBody)
223
+ const paramsToObject = new ParamsToObject(unparsedParams)
224
+ const newParams = paramsToObject.toObject()
225
+ const incorporator = new Incorporator({objects: [this.params, newParams]})
226
+
227
+ incorporator.merge()
228
+ }
229
+ }