velocious 1.0.19 → 1.0.21

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
@@ -40,3 +40,9 @@ npx velocious g:model Option
40
40
  ```bash
41
41
  npm test
42
42
  ```
43
+
44
+ # Running a server
45
+
46
+ ```bash
47
+ npx velocious server
48
+ ```
package/bin/velocious.js CHANGED
@@ -6,3 +6,5 @@ const processArgs = process.argv.slice(2)
6
6
  const cli = new Cli({processArgs})
7
7
 
8
8
  await cli.execute()
9
+
10
+ process.exit(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.19",
6
+ "version": "1.0.21",
7
7
  "main": "index.js",
8
8
  "scripts": {
9
9
  "test": "jasmine",
@@ -18,11 +18,12 @@ describe("Cli - Commands - db:migrate", () => {
18
18
  await db.query("DROP TABLE IF EXISTS tasks")
19
19
  await db.query("DROP TABLE IF EXISTS project_translations")
20
20
  await db.query("DROP TABLE IF EXISTS projects")
21
+ await db.query("DROP TABLE IF EXISTS schema_migrations")
21
22
  })
22
23
 
23
24
  await cli.execute()
24
25
 
25
- let projectForeignKey, tablesResult
26
+ let projectForeignKey, schemaMigrations, tablesResult
26
27
 
27
28
  await dbPool.withConnection(async (db) => {
28
29
  const tables = await db.getTables()
@@ -33,12 +34,15 @@ describe("Cli - Commands - db:migrate", () => {
33
34
  const foreignKeys = await table.getForeignKeys()
34
35
 
35
36
  projectForeignKey = foreignKeys.find((foreignKey) => foreignKey.getColumnName() == "project_id")
37
+
38
+ schemaMigrations = (await db.query("SELECT * FROM schema_migrations ORDER BY version")).map((schemaMigration) => schemaMigration.version)
36
39
  })
37
40
 
38
41
  expect(tablesResult).toEqual(
39
42
  [
40
43
  "project_translations",
41
44
  "projects",
45
+ "schema_migrations",
42
46
  "tasks"
43
47
  ]
44
48
  )
@@ -47,5 +51,7 @@ describe("Cli - Commands - db:migrate", () => {
47
51
  expect(projectForeignKey.getColumnName()).toEqual("project_id")
48
52
  expect(projectForeignKey.getReferencedTableName()).toEqual("projects")
49
53
  expect(projectForeignKey.getReferencedColumnName()).toEqual("id")
54
+
55
+ expect(schemaMigrations).toEqual(["20230728075328", "20230728075329", "20250605133926"])
50
56
  })
51
57
  })
@@ -1,6 +1,7 @@
1
1
  import BaseCommand from "../../base-command.js"
2
2
  import fs from "node:fs/promises"
3
3
  import * as inflection from "inflection"
4
+ import Migrator from "../../../database/migrator.js"
4
5
 
5
6
  export default class DbMigrate extends BaseCommand {
6
7
  async execute() {
@@ -8,6 +9,10 @@ export default class DbMigrate extends BaseCommand {
8
9
  const migrationsPath = `${projectPath}/src/database/migrations`
9
10
  let files = await fs.readdir(migrationsPath)
10
11
 
12
+ this.migrator = new Migrator({configuration: this.configuration})
13
+
14
+ await this.migrator.prepare()
15
+
11
16
  files = files
12
17
  .map((file) => {
13
18
  const match = file.match(/^(\d{14})-(.+)\.js$/)
@@ -16,7 +21,7 @@ export default class DbMigrate extends BaseCommand {
16
21
 
17
22
  const date = parseInt(match[1])
18
23
  const migrationName = match[2]
19
- const migrationClassName = inflection.camelize(migrationName)
24
+ const migrationClassName = inflection.camelize(migrationName.replaceAll("-", "_"))
20
25
 
21
26
  return {
22
27
  file,
@@ -29,7 +34,9 @@ export default class DbMigrate extends BaseCommand {
29
34
  .sort((migration1, migration2) => migration1.date - migration2.date)
30
35
 
31
36
  for (const migration of files) {
32
- await this.runMigrationFile(migration)
37
+ if (!this.migrator.hasRunMigrationVersion(migration.date)) {
38
+ await this.runMigrationFile(migration)
39
+ }
33
40
  }
34
41
  }
35
42
 
@@ -57,7 +64,9 @@ export default class DbMigrate extends BaseCommand {
57
64
  .sort((migration1, migration2) => migration1.date - migration2.date)
58
65
 
59
66
  for (const migration of files) {
60
- await this.runMigrationFileFromRequireContext(migration, requireContext)
67
+ if (!this.migrator.hasRunMigrationVersion(migration.date)) {
68
+ await this.runMigrationFileFromRequireContext(migration, requireContext)
69
+ }
61
70
  }
62
71
  }
63
72
 
@@ -65,7 +74,7 @@ export default class DbMigrate extends BaseCommand {
65
74
  if (!this.configuration) throw new Error("No configuration set")
66
75
  if (!this.configuration.isDatabasePoolInitialized()) await this.configuration.initializeDatabasePool()
67
76
 
68
- await this.configuration.getDatabasePool().withConnection(async () => {
77
+ await this.configuration.getDatabasePool().withConnection(async (db) => {
69
78
  const MigrationClass = requireContext(migration.file).default
70
79
  const migrationInstance = new MigrationClass({
71
80
  configuration: this.configuration
@@ -78,6 +87,8 @@ export default class DbMigrate extends BaseCommand {
78
87
  } else {
79
88
  throw new Error(`'change' or 'up' didn't exist on migration: ${migration.file}`)
80
89
  }
90
+
91
+ await db.insert({tableName: "schema_migrations", data: {version: migration.date}})
81
92
  })
82
93
  }
83
94
 
@@ -85,7 +96,7 @@ export default class DbMigrate extends BaseCommand {
85
96
  if (!this.configuration) throw new Error("No configuration set")
86
97
  if (!this.configuration.isDatabasePoolInitialized()) await this.configuration.initializeDatabasePool()
87
98
 
88
- await this.configuration.getDatabasePool().withConnection(async () => {
99
+ await this.configuration.getDatabasePool().withConnection(async (db) => {
89
100
  const migrationImport = await import(migration.fullPath)
90
101
  const MigrationClass = migrationImport.default
91
102
  const migrationInstance = new MigrationClass({
@@ -99,6 +110,8 @@ export default class DbMigrate extends BaseCommand {
99
110
  } else {
100
111
  throw new Error(`'change' or 'up' didn't exist on migration: ${migration.file}`)
101
112
  }
113
+
114
+ await db.insert({tableName: "schema_migrations", data: {version: migration.date}})
102
115
  })
103
116
  }
104
117
  }
@@ -22,7 +22,9 @@ export default class VelociousCliCommandsInit extends BaseCommand {
22
22
  ]
23
23
  const paths = [
24
24
  projectConfigPath,
25
- `${projectPath}/database/migrations`
25
+ `${projectPath}/database/migrations`,
26
+ `${projectPath}/models`,
27
+ `${projectPath}/routes`
26
28
  ]
27
29
 
28
30
  if (this.args.testing) {
@@ -1,6 +1,7 @@
1
+ import Application from "../../application.js"
1
2
  import BaseCommand from "../base-command.js"
2
3
 
3
- export default class DbCreate extends BaseCommand{
4
+ export default class VelociousCliCommandsServer extends BaseCommand{
4
5
  async execute() {
5
6
  this.databasePool = this.configuration.getDatabasePool()
6
7
  this.newConfiguration = Object.assign({}, this.databasePool.getConfiguration())
@@ -10,6 +11,19 @@ export default class DbCreate extends BaseCommand{
10
11
  this.databaseConnection = await this.databasePool.spawnConnectionWithConfiguration(this.newConfiguration)
11
12
  await this.databaseConnection.connect()
12
13
 
13
- throw new Error("stub")
14
+ const host = "0.0.0.0"
15
+ const port = 3006
16
+ const application = new Application({
17
+ configuration: this.configuration,
18
+ httpServer: {
19
+ host,
20
+ port
21
+ }
22
+ })
23
+
24
+ await application.initialize()
25
+ await application.startHttpServer()
26
+
27
+ console.log(`Started Velocious HTTP server on ${host}:${port}`)
14
28
  }
15
29
  }
package/src/cli/index.js CHANGED
@@ -1,6 +1,8 @@
1
- import configurationResolver from "../configuration-resolver.js"
2
1
  import {dirname} from "path"
3
2
  import {fileURLToPath} from "url"
3
+ import fs from "fs/promises"
4
+
5
+ import configurationResolver from "../configuration-resolver.js"
4
6
  import fileExists from "../utils/file-exists.js"
5
7
 
6
8
  export default class VelociousCli {
@@ -9,11 +11,11 @@ export default class VelociousCli {
9
11
  }
10
12
 
11
13
  async execute() {
12
- const __filename = fileURLToPath(`${import.meta.url}/../..`)
13
- const __dirname = dirname(__filename)
14
+ const __filename = fileURLToPath(import.meta.url)
15
+ const basePath = await fs.realpath(`${dirname(__filename)}/../..`)
14
16
  const commandParts = this.args.processArgs[0].split(":")
15
17
  const filePaths = []
16
- let filePath = `${__dirname}/src/cli/commands`
18
+ let filePath = `${basePath}/src/cli/commands`
17
19
 
18
20
  for (let commandPart of commandParts) {
19
21
  if (commandPart == "d") commandPart = "destroy"
@@ -11,8 +11,6 @@ export default class VelociousConfiguration {
11
11
  constructor({database, debug, directory, initializeModels, locale, localeFallbacks, locales, ...restArgs}) {
12
12
  restArgsError(restArgs)
13
13
 
14
- if (!initializeModels) throw new Error("initializeModels wasn't given")
15
-
16
14
  this.database = database
17
15
  this.debug = debug
18
16
  this._directory = directory
@@ -99,7 +97,10 @@ export default class VelociousConfiguration {
99
97
  isInitialized = () => this._isInitialized
100
98
 
101
99
  async initialize() {
102
- await this._initializeModels({configuration: this})
100
+ if (this._initializeModels) {
101
+ await this._initializeModels({configuration: this})
102
+ }
103
+
103
104
  this._isInitialized = true
104
105
  }
105
106
 
package/src/controller.js CHANGED
@@ -1,32 +1,46 @@
1
- import {digg, digs} from "diggerize"
1
+ import {digs} from "diggerize"
2
2
  import ejs from "ejs"
3
+ import * as inflection from "inflection"
4
+ import restArgsError from "./utils/rest-args-error.js"
3
5
 
4
6
  export default class VelociousController {
5
- constructor({configuration, params, request, response}) {
7
+ constructor({action, configuration, controller, params, request, response, viewPath}) {
8
+ if (!action) throw new Error("No action given")
6
9
  if (!configuration) throw new Error("No configuration given")
10
+ if (!controller) throw new Error("No controller given")
7
11
  if (!params) throw new Error("No params given")
8
12
  if (!request) throw new Error("No request given")
9
13
  if (!response) throw new Error("No response given")
14
+ if (!viewPath) throw new Error("No viewPath given")
10
15
 
16
+ this._action = action
17
+ this._controller = controller
11
18
  this._configuration = configuration
12
19
  this._params = params
13
20
  this._request = request
14
21
  this._response = response
15
22
  this.viewParams = {}
23
+ this._viewPath = viewPath
16
24
  }
17
25
 
18
26
  params = () => this._params
19
27
 
20
- render(args = {}) {
21
- if (args.json) {
22
- return this.renderJsonArg(args)
28
+ render({json, status, ...restArgs} = {}) {
29
+ restArgsError(restArgs)
30
+
31
+ if (json) {
32
+ return this.renderJsonArg(json)
33
+ }
34
+
35
+ if (status) {
36
+ this._response.setStatus(status)
23
37
  }
24
38
 
25
39
  return this.renderView()
26
40
  }
27
41
 
28
- renderJsonArg(args) {
29
- const body = JSON.stringify(args.json)
42
+ renderJsonArg(json) {
43
+ const body = JSON.stringify(json)
30
44
 
31
45
  this._response.addHeader("Content-Type", "application/json")
32
46
  this._response.setBody(body)
@@ -34,10 +48,7 @@ export default class VelociousController {
34
48
 
35
49
  renderView() {
36
50
  return new Promise((resolve, reject) => {
37
- const actionName = digg(this, "_params", "action")
38
- const controllerName = digg(this, "_params", "controller")
39
- const directory = digg(this, "_configuration").getDirectory()
40
- const viewPath = `${directory}/src/routes/${controllerName}/${actionName}.ejs`
51
+ const viewPath = `${this._viewPath}/${inflection.dasherize(inflection.underscore(this._action))}.ejs`
41
52
  const {viewParams} = digs(this, "viewParams")
42
53
 
43
54
  ejs.renderFile(viewPath, viewParams, {}, (err, str) => {
@@ -14,12 +14,14 @@ export default class VeoliciousHttpServerClient {
14
14
  this.clientCount = clientCount
15
15
  this.configuration = configuration
16
16
  this.onExecuteRequest = onExecuteRequest
17
+ this.requestRunners = []
17
18
  }
18
19
 
19
20
  executeCurrentRequest = () => {
20
21
  logger(this, "executeCurrentRequest")
21
22
 
22
- this.state = "response"
23
+ // We are done parsing the given request and can theoretically start parsing a new one, before the current request is done - so reset the state.
24
+ this.state = "initial"
23
25
 
24
26
  const requestRunner = new RequestRunner({
25
27
  configuration: this.configuration,
@@ -27,7 +29,9 @@ export default class VeoliciousHttpServerClient {
27
29
  routes: this.routes
28
30
  })
29
31
 
30
- requestRunner.events.on("done", this.sendResponse)
32
+ this.requestRunners.push(requestRunner)
33
+
34
+ requestRunner.events.on("done", this.requestDone)
31
35
  requestRunner.run()
32
36
  }
33
37
 
@@ -42,7 +46,24 @@ export default class VeoliciousHttpServerClient {
42
46
  } else if (this.state == "requestStarted") {
43
47
  this.currentRequest.feed(data)
44
48
  } else {
45
- throw new Error(`Unknown state: ${this.state}`)
49
+ throw new Error(`Unknown state for client: ${this.state}`)
50
+ }
51
+ }
52
+
53
+ requestDone = () => {
54
+ this.sendDoneRequests()
55
+ }
56
+
57
+ sendDoneRequests() {
58
+ while (true) {
59
+ const requestRunner = this.requestRunners[0]
60
+
61
+ if (requestRunner?.getState() == "done") {
62
+ this.requestRunners.shift()
63
+ this.sendResponse(requestRunner)
64
+ } else {
65
+ break
66
+ }
46
67
  }
47
68
  }
48
69
 
@@ -58,7 +79,7 @@ export default class VeoliciousHttpServerClient {
58
79
 
59
80
  let headers = ""
60
81
 
61
- headers += "HTTP/1.1 200 OK\r\n"
82
+ headers += `HTTP/1.1 ${response.getStatusCode()} ${response.getStatusMessage()}\r\n`
62
83
 
63
84
  for (const headerKey in response.headers) {
64
85
  for (const headerValue of response.headers[headerKey]) {
@@ -82,7 +82,7 @@ export default class RequestBuffer {
82
82
 
83
83
  break
84
84
  default:
85
- console.error(`Unknown state: ${this.state}`)
85
+ console.error(`Unknown state for request buffer: ${this.state}`)
86
86
  }
87
87
  }
88
88
  }
@@ -128,7 +128,7 @@ export default class RequestBuffer {
128
128
  this.setState("multi-part-form-data-body")
129
129
  }
130
130
  } else {
131
- throw new Error(`Unknown state: ${this.state}`)
131
+ throw new Error(`Unknown state parsing line: ${this.state}`)
132
132
  }
133
133
  }
134
134
 
@@ -200,7 +200,7 @@ export default class RequestBuffer {
200
200
  }
201
201
 
202
202
  completeRequest = () => {
203
- this.state = "completed"
203
+ this.state = "status" // Reset state to new request
204
204
 
205
205
  if (this.getHeader("content-type")?.value?.startsWith("application/json")) {
206
206
  this.parseApplicationJsonParams()
@@ -13,8 +13,11 @@ export default class VelociousHttpServerClientRequestRunner {
13
13
  this.configuration = configuration
14
14
  this.request = request
15
15
  this.response = new Response({configuration})
16
+ this.state = "running"
16
17
  }
17
18
 
19
+ getState = () => this.state
20
+
18
21
  async run() {
19
22
  if (!this.request) throw new Error("No request?")
20
23
 
@@ -27,6 +30,7 @@ export default class VelociousHttpServerClientRequestRunner {
27
30
  })
28
31
 
29
32
  await routesResolver.resolve()
33
+ this.state = "done"
30
34
  this.events.emit("done", this)
31
35
  }
32
36
  }
@@ -22,7 +22,24 @@ export default class VelociousHttpServerClientResponse {
22
22
  throw new Error("No body has been set")
23
23
  }
24
24
 
25
+ getStatusCode() {
26
+ return this.statusCode || 200
27
+ }
28
+
29
+ getStatusMessage() {
30
+ return this.statusMessage || "OK"
31
+ }
32
+
25
33
  setBody(value) {
26
34
  this.body = value
27
35
  }
36
+
37
+ setStatus(status) {
38
+ if (status == "not-found" || status == 404) {
39
+ this.statusCode = 404
40
+ this.statusMessage = "Not Found"
41
+ } else {
42
+ throw new Error(`Unhandled status: ${status}`)
43
+ }
44
+ }
28
45
  }
@@ -0,0 +1,7 @@
1
+ import Controller from "../../../controller.js"
2
+
3
+ export default class BuiltInErrorsController extends Controller {
4
+ async notFound() {
5
+ this.render({status: "not-found"})
6
+ }
7
+ }
@@ -0,0 +1 @@
1
+ Not found!
@@ -1,4 +1,7 @@
1
1
  import {digg, digs} from "diggerize"
2
+ import {fileURLToPath} from "url"
3
+ import fs from "fs/promises"
4
+ import {dirname} from "path"
2
5
 
3
6
  export default class VelociousRoutesResolver {
4
7
  constructor({configuration, request, response}) {
@@ -13,36 +16,49 @@ export default class VelociousRoutesResolver {
13
16
  }
14
17
 
15
18
  async resolve() {
19
+ let controllerPath
16
20
  let currentRoute = digg(this, "configuration", "routes", "rootRoute")
17
21
  let currentPath = this.request.path()
22
+ let viewPath
18
23
 
19
24
  const matchResult = this.matchPathWithRoutes(currentRoute, currentPath)
25
+ let action = this.params.action
26
+ let controller = this.params.controller
27
+
28
+ if (!matchResult) {
29
+ const __filename = fileURLToPath(import.meta.url)
30
+ const __dirname = dirname(__filename)
31
+
32
+ controller = "errors"
33
+ controllerPath = "./built-in/errors/controller.js"
34
+ action = "notFound"
35
+ viewPath = await fs.realpath(`${__dirname}/built-in/errors`) // eslint-disable-line no-undef
36
+ } else if (action && controller) {
37
+ controllerPath = `${this.configuration.getDirectory()}/src/routes/${controller}/controller.js`
38
+ viewPath = `${this.configuration.getDirectory()}/src/routes/${controller}`
39
+ } else {
40
+ throw new Error(`Matched the route but didn't know what to do with it: ${currentPath} (action: ${action}, controller: ${controller}, params: ${JSON.stringify(this.params)})`)
41
+ }
20
42
 
21
- if (!matchResult) throw new Error(`Couldn't match a route with the given path: ${currentPath}`)
22
-
23
- if (this.params.action && this.params.controller) {
24
- const controllerPath = `${this.configuration.getDirectory()}/src/routes/${digg(this, "params", "controller")}/controller.js`
25
- const controllerClassImport = await import(controllerPath)
26
- const controllerClass = controllerClassImport.default
27
- const controllerInstance = new controllerClass({
28
- configuration: this.configuration,
29
- params: this.params,
30
- request: this.request,
31
- response: this.response
32
- })
33
-
34
- if (!(this.params.action in controllerInstance)) {
35
- throw new Error(`Missing action on controller: ${this.params.controller}#${this.params.action}`)
36
- }
37
-
38
- await this.configuration.getDatabasePool().withConnection(async () => {
39
- await controllerInstance[this.params.action]()
40
- })
41
-
42
- return
43
+ const controllerClassImport = await import(controllerPath)
44
+ const controllerClass = controllerClassImport.default
45
+ const controllerInstance = new controllerClass({
46
+ action,
47
+ configuration: this.configuration,
48
+ controller,
49
+ params: this.params,
50
+ request: this.request,
51
+ response: this.response,
52
+ viewPath
53
+ })
54
+
55
+ if (!(action in controllerInstance)) {
56
+ throw new Error(`Missing action on controller: ${controller}#${action}`)
43
57
  }
44
58
 
45
- throw new Error(`Matched the route but didn't know what to do with it: ${currentPath}`)
59
+ await this.configuration.getDatabasePool().withConnection(async () => {
60
+ await controllerInstance[action]()
61
+ })
46
62
  }
47
63
 
48
64
  matchPathWithRoutes(route, path) {