velocious 1.0.5 → 1.0.7

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 (173) hide show
  1. package/bin/{velocious.mjs → velocious.js} +1 -1
  2. package/package.json +7 -5
  3. package/peak_flow.yml +1 -1
  4. package/spec/cli/commands/db/{create-spec.mjs → create-spec.js} +3 -3
  5. package/spec/cli/commands/db/{migrate-spec.mjs → migrate-spec.js} +4 -2
  6. package/spec/cli/commands/destroy/{migration-spec.mjs → migration-spec.js} +2 -2
  7. package/spec/cli/commands/generate/{migration-spec.mjs → migration-spec.js} +3 -3
  8. package/spec/cli/commands/{init-spec.mjs → init-spec.js} +6 -6
  9. package/spec/cli/commands/test/{test-files-finder-spec.mjs → test-files-finder-spec.js} +2 -2
  10. package/spec/database/connection/drivers/mysql/{query-parser-spec.mjs → query-parser-spec.js} +6 -6
  11. package/spec/database/drivers/mysql/{connection-spec.mjs → connection-spec.js} +2 -2
  12. package/spec/database/record/create-spec.js +23 -0
  13. package/spec/database/record/{destroy-spec.mjs → destroy-spec.js} +2 -2
  14. package/spec/database/record/{find-spec.mjs → find-spec.js} +2 -3
  15. package/spec/database/record/query-spec.js +37 -0
  16. package/spec/database/record/{update-spec.mjs → update-spec.js} +2 -2
  17. package/spec/dummy/{index.mjs → index.js} +16 -4
  18. package/spec/dummy/src/config/configuration.example.js +36 -0
  19. package/spec/dummy/src/config/configuration.peakflow.js +37 -0
  20. package/spec/dummy/src/config/{routes.mjs → routes.js} +1 -1
  21. package/spec/dummy/src/database/migrations/{20230728075328-create-projects.mjs → 20230728075328-create-projects.js} +1 -2
  22. package/spec/dummy/src/database/migrations/{20230728075329-create-tasks.mjs → 20230728075329-create-tasks.js} +1 -2
  23. package/spec/dummy/src/database/migrations/20250605133926-create-project-translations.js +16 -0
  24. package/spec/dummy/src/models/project.js +9 -0
  25. package/spec/dummy/src/models/task.js +8 -0
  26. package/spec/dummy/src/routes/tasks/{controller.mjs → controller.js} +2 -2
  27. package/spec/http-server/{client-spec.mjs → client-spec.js} +3 -3
  28. package/spec/http-server/{get-spec.mjs → get-spec.js} +1 -1
  29. package/spec/http-server/{post-spec.mjs → post-spec.js} +2 -2
  30. package/spec/support/jasmine.json +2 -2
  31. package/src/{application.mjs → application.js} +3 -3
  32. package/src/big-brother.js +37 -0
  33. package/src/cli/commands/db/{create.mjs → create.js} +9 -7
  34. package/src/cli/commands/db/{migrate.mjs → migrate.js} +4 -5
  35. package/src/cli/commands/destroy/{migration.mjs → migration.js} +2 -2
  36. package/src/cli/commands/generate/{migration.mjs → migration.js} +5 -5
  37. package/src/cli/commands/generate/{model.mjs → model.js} +5 -5
  38. package/src/cli/commands/{init.mjs → init.js} +6 -6
  39. package/src/cli/commands/{server.mjs → server.js} +1 -1
  40. package/src/cli/commands/test/{index.mjs → index.js} +3 -3
  41. package/src/cli/commands/test/{test-files-finder.mjs → test-files-finder.js} +1 -1
  42. package/src/cli/{index.mjs → index.js} +4 -4
  43. package/src/{configuration-resolver.mjs → configuration-resolver.js} +2 -2
  44. package/src/{configuration.mjs → configuration.js} +39 -3
  45. package/src/database/drivers/{base.mjs → base.js} +23 -4
  46. package/src/database/drivers/mysql/column.js +8 -0
  47. package/src/database/drivers/mysql/{index.mjs → index.js} +44 -12
  48. package/src/database/drivers/{sqlite/options.mjs → mysql/options.js} +2 -1
  49. package/src/database/drivers/mysql/query-parser.js +4 -0
  50. package/src/database/drivers/mysql/sql/{create-database.mjs → create-database.js} +1 -1
  51. package/src/database/drivers/{sqlite/sql/create-table.mjs → mysql/sql/create-table.js} +1 -1
  52. package/src/database/drivers/mysql/sql/{delete.mjs → delete.js} +1 -1
  53. package/src/database/drivers/{sqlite/sql/insert.mjs → mysql/sql/insert.js} +1 -1
  54. package/src/database/drivers/mysql/sql/{update.mjs → update.js} +1 -1
  55. package/src/database/drivers/mysql/table.js +25 -0
  56. package/src/database/drivers/sqlite/base.js +108 -0
  57. package/src/database/drivers/sqlite/column.js +10 -0
  58. package/src/database/drivers/sqlite/{index.native.mjs → index.native.js} +19 -22
  59. package/src/database/drivers/sqlite/index.web.js +55 -0
  60. package/src/database/drivers/{mysql/options.mjs → sqlite/options.js} +3 -2
  61. package/src/database/drivers/sqlite/query-parser.js +4 -0
  62. package/src/database/drivers/sqlite/query.native.js +24 -0
  63. package/src/database/drivers/sqlite/query.web.js +34 -0
  64. package/src/database/drivers/sqlite/sql/create-index.js +4 -0
  65. package/src/database/drivers/{mysql/sql/create-table.mjs → sqlite/sql/create-table.js} +1 -1
  66. package/src/database/drivers/sqlite/sql/{delete.mjs → delete.js} +1 -1
  67. package/src/database/drivers/{mysql/sql/insert.mjs → sqlite/sql/insert.js} +1 -1
  68. package/src/database/drivers/sqlite/sql/{update.mjs → update.js} +1 -1
  69. package/src/database/drivers/sqlite/table.js +24 -0
  70. package/src/database/initializer-from-require-context.js +21 -0
  71. package/src/database/{migrate-from-require-context.mjs → migrate-from-require-context.js} +2 -2
  72. package/src/database/migration/index.js +50 -0
  73. package/src/database/{migrator.mjs → migrator.js} +4 -2
  74. package/src/database/pool/{async-tracked-multi-connection.mjs → async-tracked-multi-connection.js} +1 -1
  75. package/src/database/pool/{base.mjs → base.js} +6 -1
  76. package/src/database/pool/{single-multi-use.mjs → single-multi-use.js} +1 -1
  77. package/src/database/query/{base.mjs → base.js} +2 -1
  78. package/src/database/query/{create-database-base.mjs → create-database-base.js} +1 -1
  79. package/src/database/query/create-index-base.js +50 -0
  80. package/src/database/query/create-table-base.js +92 -0
  81. package/src/database/query/{delete-base.mjs → delete-base.js} +1 -1
  82. package/src/database/query/{from-plain.mjs → from-plain.js} +1 -1
  83. package/src/database/query/{from-table.mjs → from-table.js} +1 -1
  84. package/src/database/query/index.js +206 -0
  85. package/src/database/query/{join-plain.mjs → join-plain.js} +1 -1
  86. package/src/database/query/{order-plain.mjs → order-plain.js} +1 -1
  87. package/src/database/query/preloader/belongs-to.js +52 -0
  88. package/src/database/query/preloader/has-many.js +55 -0
  89. package/src/database/query/preloader.js +41 -0
  90. package/src/database/query/{select-plain.mjs → select-plain.js} +1 -1
  91. package/src/database/query/{select-table-and-column.mjs → select-table-and-column.js} +1 -1
  92. package/src/database/query/where-base.js +9 -0
  93. package/src/database/query/where-hash.js +35 -0
  94. package/src/database/query/where-plain.js +13 -0
  95. package/src/database/query-parser/base-query-parser.js +33 -0
  96. package/src/database/query-parser/group-parser.js +40 -0
  97. package/src/database/query-parser/joins-parser.js +71 -0
  98. package/src/database/query-parser/limit-parser.js +40 -0
  99. package/src/database/query-parser/{options.mjs → options.js} +2 -1
  100. package/src/database/query-parser/order-parser.js +39 -0
  101. package/src/database/query-parser/{select-parser.mjs → select-parser.js} +5 -1
  102. package/src/database/query-parser/where-parser.js +39 -0
  103. package/src/database/record/index.js +622 -0
  104. package/src/database/record/instance-relationships/base.js +28 -0
  105. package/src/database/record/instance-relationships/belongs-to.js +20 -0
  106. package/src/database/record/instance-relationships/has-many.js +47 -0
  107. package/src/database/record/relationships/base.js +32 -0
  108. package/src/database/record/relationships/belongs-to.js +12 -0
  109. package/src/database/record/relationships/has-many.js +12 -0
  110. package/src/database/table-data/{index.mjs → index.js} +15 -25
  111. package/src/http-server/client/{index.mjs → index.js} +3 -3
  112. package/src/http-server/client/request-buffer/{index.mjs → index.js} +4 -4
  113. package/src/http-server/client/{request-parser.mjs → request-parser.js} +2 -2
  114. package/src/http-server/client/{request-runner.mjs → request-runner.js} +3 -3
  115. package/src/http-server/client/{request.mjs → request.js} +1 -1
  116. package/src/http-server/{index.mjs → index.js} +3 -3
  117. package/src/http-server/{server-client.mjs → server-client.js} +1 -1
  118. package/src/http-server/worker-handler/{index.mjs → index.js} +2 -2
  119. package/src/http-server/worker-handler/{worker-script.mjs → worker-script.js} +1 -1
  120. package/src/http-server/worker-handler/{worker-thread.mjs → worker-thread.js} +12 -9
  121. package/src/routes/{app-routes.mjs → app-routes.js} +1 -1
  122. package/src/routes/{base-route.mjs → base-route.js} +2 -2
  123. package/src/routes/{get-route.mjs → get-route.js} +1 -1
  124. package/src/routes/{index.mjs → index.js} +1 -1
  125. package/src/routes/{resolver.mjs → resolver.js} +1 -1
  126. package/src/routes/{resource-route.mjs → resource-route.js} +1 -1
  127. package/src/routes/{root-route.mjs → root-route.js} +1 -1
  128. package/src/templates/{configuration.mjs → configuration.js} +3 -3
  129. package/src/templates/{generate-migration.mjs → generate-migration.js} +1 -1
  130. package/src/templates/generate-model.js +6 -0
  131. package/src/templates/{routes.mjs → routes.js} +1 -1
  132. package/src/utils/rest-args-error.js +9 -0
  133. package/spec/database/record/create-spec.mjs +0 -14
  134. package/spec/dummy/src/config/configuration.example.mjs +0 -21
  135. package/spec/dummy/src/config/configuration.peakflow.mjs +0 -22
  136. package/spec/dummy/src/models/task.mjs +0 -4
  137. package/src/database/drivers/mysql/query-parser.mjs +0 -25
  138. package/src/database/drivers/sqlite/base.mjs +0 -36
  139. package/src/database/drivers/sqlite/index.web.mjs +0 -45
  140. package/src/database/drivers/sqlite/query-parser.mjs +0 -25
  141. package/src/database/drivers/sqlite/query.native.mjs +0 -9
  142. package/src/database/drivers/sqlite/query.web.mjs +0 -9
  143. package/src/database/drivers/sqlite/table.mjs +0 -9
  144. package/src/database/migration/index.mjs +0 -18
  145. package/src/database/query/create-table-base.mjs +0 -69
  146. package/src/database/query/index.mjs +0 -144
  147. package/src/database/query-parser/joins-parser.mjs +0 -30
  148. package/src/database/record/index.mjs +0 -187
  149. package/src/templates/generate-model.mjs +0 -4
  150. /package/{index.mjs → index.js} +0 -0
  151. /package/spec/dummy/{dummy-directory.mjs → dummy-directory.js} +0 -0
  152. /package/src/cli/{base-command.mjs → base-command.js} +0 -0
  153. /package/src/cli/commands/test/{test-runner.mjs → test-runner.js} +0 -0
  154. /package/src/{controller.mjs → controller.js} +0 -0
  155. /package/src/database/drivers/mysql/{connect-connection.mjs → connect-connection.js} +0 -0
  156. /package/src/database/drivers/mysql/{query.mjs → query.js} +0 -0
  157. /package/src/database/{handler.mjs → handler.js} +0 -0
  158. /package/src/database/query/{from-base.mjs → from-base.js} +0 -0
  159. /package/src/database/query/{insert-base.mjs → insert-base.js} +0 -0
  160. /package/src/database/query/{join-base.mjs → join-base.js} +0 -0
  161. /package/src/database/query/{order-base.mjs → order-base.js} +0 -0
  162. /package/src/database/query/{select-base.mjs → select-base.js} +0 -0
  163. /package/src/database/query/{update-base.mjs → update-base.js} +0 -0
  164. /package/src/database/query-parser/{from-parser.mjs → from-parser.js} +0 -0
  165. /package/src/database/record/{record-not-found-error.mjs → record-not-found-error.js} +0 -0
  166. /package/src/{error-logger.mjs → error-logger.js} +0 -0
  167. /package/src/http-server/client/{params-to-object.mjs → params-to-object.js} +0 -0
  168. /package/src/http-server/client/request-buffer/{form-data-part.mjs → form-data-part.js} +0 -0
  169. /package/src/http-server/client/request-buffer/{header.mjs → header.js} +0 -0
  170. /package/src/http-server/client/{response.mjs → response.js} +0 -0
  171. /package/src/{logger.mjs → logger.js} +0 -0
  172. /package/src/spec/{index.mjs → index.js} +0 -0
  173. /package/src/utils/{file-exists.mjs → file-exists.js} +0 -0
@@ -35,7 +35,11 @@ export default class VelociousDatabaseQueryParserSelectParser {
35
35
  }
36
36
 
37
37
  if (query._selects.length == 0) {
38
- sql += "*"
38
+ if (query.modelClass) {
39
+ sql += `${query.modelClass.connection().quoteTable(query.modelClass.tableName())}.*`
40
+ } else {
41
+ sql += "*"
42
+ }
39
43
  }
40
44
 
41
45
  return sql
@@ -0,0 +1,39 @@
1
+ import {digs} from "diggerize"
2
+
3
+ export default class VelocuiousDatabaseQueryParserWhereParser {
4
+ constructor({pretty, query}) {
5
+ this.pretty = pretty
6
+ this.query = query
7
+ }
8
+
9
+ toSql() {
10
+ const {pretty, query} = digs(this, "pretty", "query")
11
+ let sql = ""
12
+
13
+ if (query._wheres.length == 0) return sql
14
+
15
+ if (pretty) {
16
+ sql += "\n\n"
17
+ } else {
18
+ sql += " "
19
+ }
20
+
21
+ sql += "WHERE"
22
+
23
+ for (const whereKey in query._wheres) {
24
+ const where = query._wheres[whereKey]
25
+
26
+ if (whereKey > 0) sql += " &&"
27
+
28
+ if (pretty) {
29
+ sql += "\n "
30
+ } else {
31
+ sql += " "
32
+ }
33
+
34
+ sql += where.toSql()
35
+ }
36
+
37
+ return sql
38
+ }
39
+ }
@@ -0,0 +1,622 @@
1
+ import BelongsToInstanceRelationship from "./instance-relationships/belongs-to.js"
2
+ import BelongsToRelationship from "./relationships/belongs-to.js"
3
+ import Configuration from "../../configuration.js"
4
+ import FromTable from "../query/from-table.js"
5
+ import Handler from "../handler.js"
6
+ import HasManyRelationship from "./relationships/has-many.js"
7
+ import HasManyInstanceRelationship from "./instance-relationships/has-many.js"
8
+ import * as inflection from "inflection"
9
+ import Query from "../query/index.js"
10
+ import RecordNotFoundError from "./record-not-found-error.js"
11
+
12
+ export default class VelociousDatabaseRecord {
13
+ static _relationshipExists(relationshipName) {
14
+ if (this._relationships && relationshipName in this._relationships) {
15
+ return true
16
+ }
17
+
18
+ return false
19
+ }
20
+
21
+ static _defineRelationship(relationshipName, data) {
22
+ if (!this._relationships) this._relationships = {}
23
+ if (this._relationshipExists(relationshipName)) throw new Error(`Relationship ${relationshipName} already exists`)
24
+
25
+ const actualData = Object.assign(
26
+ {
27
+ modelClass: this,
28
+ relationshipName,
29
+ type: "hasMany"
30
+ },
31
+ data
32
+ )
33
+
34
+ if (!actualData.className && !actualData.klass) {
35
+ actualData.className = inflection.camelize(inflection.singularize(relationshipName))
36
+ }
37
+
38
+ let relationship
39
+
40
+ if (actualData.type == "belongsTo") {
41
+ relationship = new BelongsToRelationship(actualData)
42
+
43
+ const buildMethodName = `build${inflection.camelize(relationshipName)}`
44
+
45
+ this.prototype[relationshipName] = function () {
46
+ const relationship = this.getRelationshipByName(relationshipName)
47
+
48
+ return relationship.loaded()
49
+ }
50
+
51
+ this.prototype[buildMethodName] = function (attributes) {
52
+ const relationship = this.getRelationshipByName(relationshipName)
53
+ const record = relationship.build(attributes)
54
+
55
+ return record
56
+ }
57
+ } else if (actualData.type == "hasMany") {
58
+ relationship = new HasManyRelationship(actualData)
59
+
60
+ this.prototype[relationshipName] = function () {
61
+ return this.getRelationshipByName(relationshipName)
62
+ }
63
+ } else {
64
+ throw new Error(`Unknown relationship type: ${actualData.type}`)
65
+ }
66
+
67
+ this._relationships[relationshipName] = relationship
68
+ }
69
+
70
+ static getRelationshipByName(relationshipName) {
71
+ if (!this._relationships) this._relationships = {}
72
+
73
+ const relationship = this._relationships[relationshipName]
74
+
75
+ if (!relationship) throw new Error(`No relationship by that name: ${relationshipName}`)
76
+
77
+ return relationship
78
+ }
79
+
80
+ getRelationshipByName(relationshipName) {
81
+ if (!this._instanceRelationships) this._instanceRelationships = {}
82
+
83
+ if (!(relationshipName in this._instanceRelationships)) {
84
+ const modelClassRelationship = this.constructor.getRelationshipByName(relationshipName)
85
+ let instanceRelationship
86
+
87
+ if (modelClassRelationship.getType() == "belongsTo") {
88
+ instanceRelationship = new BelongsToInstanceRelationship({model: this, relationship: modelClassRelationship})
89
+ } else if (modelClassRelationship.getType() == "hasMany") {
90
+ instanceRelationship = new HasManyInstanceRelationship({model: this, relationship: modelClassRelationship})
91
+ } else {
92
+ throw new Error(`Unknown relationship type: ${modelClassRelationship.getType()}`)
93
+ }
94
+
95
+ this._instanceRelationships[relationshipName] = instanceRelationship
96
+ }
97
+
98
+ return this._instanceRelationships[relationshipName]
99
+ }
100
+
101
+ static belongsTo(relationshipName) {
102
+ this._defineRelationship(relationshipName, {type: "belongsTo"})
103
+ }
104
+
105
+ static connection() {
106
+ const connection = this._getConfiguration().getDatabasePoolType().current().getCurrentConnection()
107
+
108
+ if (!connection) throw new Error("No connection?")
109
+
110
+ return connection
111
+ }
112
+
113
+ static async create(attributes) {
114
+ const record = new this(attributes)
115
+
116
+ await record.save()
117
+
118
+ return record
119
+ }
120
+
121
+ static async find(recordId) {
122
+ const conditions = {}
123
+
124
+ conditions[this.primaryKey()] = recordId
125
+
126
+ const query = this.where(conditions)
127
+ const record = await query.first()
128
+
129
+ if (!record) {
130
+ throw new RecordNotFoundError(`Couldn't find ${this.name} with '${this.primaryKey()}'=${recordId}`)
131
+ }
132
+
133
+ return record
134
+ }
135
+
136
+ static _getConfiguration() {
137
+ if (!this._configuration) {
138
+ this._configuration = Configuration.current()
139
+
140
+ if (!this._configuration) {
141
+ throw new Error("Configuration hasn't been set (model class probably hasn't been initialized)")
142
+ }
143
+ }
144
+
145
+ return this._configuration
146
+ }
147
+
148
+ _getConfiguration() {
149
+ return this.constructor._getConfiguration()
150
+ }
151
+
152
+ static hasMany(relationshipName, options = {}) {
153
+ return this._defineRelationship(relationshipName, Object.assign({type: "hasMany"}, options))
154
+ }
155
+
156
+ static async initializeRecord({configuration}) {
157
+ if (!configuration) throw new Error("No configuration given")
158
+
159
+ this._configuration = configuration
160
+ this._configuration.registerModelClass(this)
161
+
162
+ this._table = await this.connection().getTableByName(this.tableName())
163
+ this._columns = await this._getTable().getColumns()
164
+ this._columnsAsHash = {}
165
+
166
+ for (const column of this._columns) {
167
+ this._columnsAsHash[column.getName()] = column
168
+
169
+ const camelizedColumnName = inflection.camelize(column.getName(), true)
170
+ const camelizedColumnNameBigFirst = inflection.camelize(column.getName())
171
+ const setterMethodName = `set${camelizedColumnNameBigFirst}`
172
+
173
+ this.prototype[camelizedColumnName] = function () {
174
+ return this.readAttribute(camelizedColumnName)
175
+ }
176
+
177
+ this.prototype[setterMethodName] = function (newValue) {
178
+ return this._setColumnAttribute(camelizedColumnName, newValue)
179
+ }
180
+ }
181
+
182
+ await this._defineTranslationMethods()
183
+ this._initialized = true
184
+ }
185
+
186
+ static isInitialized() {
187
+ if (this._initialized) return true
188
+
189
+ return false
190
+ }
191
+
192
+ static async _defineTranslationMethods() {
193
+ if (this._translations) {
194
+ const locales = this._getConfiguration().getLocales()
195
+
196
+ if (!locales) throw new Error("Locales hasn't been set in the configuration")
197
+
198
+ await this.getTranslationClass().initializeRecord({configuration: this._getConfiguration()})
199
+
200
+ for (const name in this._translations) {
201
+ const nameCamelized = inflection.camelize(name)
202
+ const setterMethodName = `set${nameCamelized}`
203
+
204
+ this.prototype[name] = function () {
205
+ const locale = this._getConfiguration().getLocale()
206
+
207
+ return this._getTranslatedAttribute(name, locale)
208
+ }
209
+
210
+ this.prototype[setterMethodName] = function (newValue) {
211
+ const locale = this._getConfiguration().getLocale()
212
+
213
+ return this._setTranslatedAttribute(name, locale, newValue)
214
+ }
215
+
216
+ for (const locale of locales) {
217
+ const localeCamelized = inflection.camelize(locale)
218
+ const getterMethodNameLocalized = `${name}${localeCamelized}`
219
+ const setterMethodNameLocalized = `${setterMethodName}${localeCamelized}`
220
+
221
+ this.prototype[getterMethodNameLocalized] = function () {
222
+ return this._getTranslatedAttribute(name, locale)
223
+ }
224
+
225
+ this.prototype[setterMethodNameLocalized] = function (newValue) {
226
+ return this._setTranslatedAttribute(name, locale, newValue)
227
+ }
228
+ }
229
+ }
230
+ }
231
+ }
232
+
233
+ getAttribute(name) {
234
+ const columnName = inflection.underscore(name)
235
+
236
+ if (!this.isNewRecord() && !(columnName in this._attributes)) {
237
+ throw new Error(`${this.constructor.name}#${name} attribute hasn't been loaded yet in ${Object.keys(this._attributes).join(", ")}`)
238
+ }
239
+
240
+ return this._attributes[columnName]
241
+ }
242
+
243
+ setAttribute(name, newValue) {
244
+ const setterName = `set${inflection.camelize(name)}`
245
+
246
+ if (!this.constructor.isInitialized()) throw new Error(`${this.constructor.name} model isn't initialized yet`)
247
+ if (!(setterName in this)) throw new Error(`No such setter method: ${this.constructor.name}#${setterName}`)
248
+
249
+ this[setterName](newValue)
250
+ }
251
+
252
+ _setColumnAttribute(name, newValue) {
253
+ const columnName = inflection.underscore(name)
254
+
255
+ if (this._attributes[columnName] != newValue) {
256
+ this._changes[columnName] = newValue
257
+ }
258
+ }
259
+
260
+ static getColumns() {
261
+ if (!this._columns) throw new Error(`${this.name} hasn't been initialized yet`)
262
+
263
+ return this._columns
264
+ }
265
+
266
+ static _getTable() {
267
+ if (!this._table) throw new Error(`${this.name} hasn't been initialized yet`)
268
+
269
+ return this._table
270
+ }
271
+
272
+ static async last() {
273
+ const query = this._newQuery().order(this.primaryKey())
274
+ const record = await query.last()
275
+
276
+ return record
277
+ }
278
+
279
+ static primaryKey() {
280
+ return "id"
281
+ }
282
+
283
+ async save() {
284
+ let result
285
+
286
+ const isNewRecord = this.isNewRecord()
287
+
288
+ await this._autoSaveBelongsToRelationships()
289
+
290
+ if (this.isPersisted()) {
291
+ result = await this._updateRecordWithChanges()
292
+ } else {
293
+ result = await this._createNewRecord()
294
+ }
295
+
296
+ await this._autoSaveHasManyRelationships({isNewRecord})
297
+
298
+ return result
299
+ }
300
+
301
+ async _autoSaveBelongsToRelationships() {
302
+ for (const relationshipName in this._instanceRelationships) {
303
+ const instanceRelationship = this._instanceRelationships[relationshipName]
304
+
305
+ if (instanceRelationship.getType() != "belongsTo") {
306
+ continue
307
+ }
308
+
309
+ const model = instanceRelationship.loaded()
310
+
311
+ if (model.isChanged()) {
312
+ await model.save()
313
+
314
+ const foreignKey = instanceRelationship.getForeignKey()
315
+
316
+ this.setAttribute(foreignKey, model.id())
317
+ instanceRelationship.setPreloaded(true)
318
+ }
319
+ }
320
+ }
321
+
322
+ async _autoSaveHasManyRelationships({isNewRecord}) {
323
+ for (const relationshipName in this._instanceRelationships) {
324
+ const instanceRelationship = this._instanceRelationships[relationshipName]
325
+
326
+ if (instanceRelationship.getType() != "hasMany") {
327
+ continue
328
+ }
329
+
330
+ let loaded = instanceRelationship._loaded
331
+
332
+ if (!Array.isArray(loaded)) loaded = [loaded]
333
+
334
+ for (const model of loaded) {
335
+ const foreignKey = instanceRelationship.getForeignKey()
336
+
337
+ model.setAttribute(foreignKey, this.id())
338
+
339
+ if (model.isChanged()) {
340
+ await model.save()
341
+ }
342
+ }
343
+
344
+ if (isNewRecord) {
345
+ instanceRelationship.setPreloaded(true)
346
+ }
347
+ }
348
+ }
349
+
350
+ static tableName() {
351
+ return inflection.underscore(inflection.pluralize(this.name))
352
+ }
353
+
354
+ static setTableName(tableName) {
355
+ this._tableName = tableName
356
+ }
357
+
358
+ static translates(...names) {
359
+ for (const name of names) {
360
+ if (!this._translations) this._translations = {}
361
+ if (name in this._translations) throw new Error(`Translation already exists: ${name}`)
362
+
363
+ this._translations[name] = {}
364
+
365
+ if (!this._relationshipExists("translations")) {
366
+ this._defineRelationship("translations", {klass: this.getTranslationClass(), type: "hasMany"})
367
+ }
368
+ }
369
+ }
370
+
371
+ static getTranslationClass() {
372
+ if (this._translationClass) return this._translationClass
373
+ if (this.tableName().endsWith("_translations")) throw new Error("Trying to define a translations class for a translation class")
374
+
375
+ const className = `${this.name}Translation`
376
+ const TranslationClass = class Translation extends VelociousDatabaseRecord {}
377
+ const belongsTo = `${inflection.camelize(inflection.singularize(this.tableName()), true)}`
378
+ const tableName = `${inflection.singularize(this.tableName())}_translations`
379
+
380
+ Object.defineProperty(TranslationClass, "name", {value: className})
381
+ TranslationClass.setTableName(tableName)
382
+ TranslationClass.belongsTo(belongsTo)
383
+
384
+ this._translationClass = TranslationClass
385
+
386
+ return this._translationClass
387
+ }
388
+
389
+ static getTranslationsTableName() {
390
+ return `${inflection.singularize(this.tableName())}_translations`
391
+ }
392
+
393
+ static async hasTranslationsTable() {
394
+ try {
395
+ await this.connection().getTableByName(this.getTranslationsTableName())
396
+
397
+ return true
398
+ } catch {
399
+ return false
400
+ }
401
+ }
402
+
403
+ _getTranslatedAttribute(name, locale) {
404
+ const translation = this.translations().loaded().find((translation) => translation.locale() == locale)
405
+
406
+ if (translation) {
407
+ return translation[name]()
408
+ }
409
+ }
410
+
411
+ _setTranslatedAttribute(name, locale, newValue) {
412
+ let translation = this.translations().loaded()?.find((translation) =>translation.locale() == locale)
413
+
414
+ if (!translation) translation = this.translations().build({locale})
415
+
416
+ const assignments = {}
417
+
418
+ assignments[name] = newValue
419
+
420
+ translation.assign(assignments)
421
+ }
422
+
423
+ static _newQuery() {
424
+ const handler = new Handler()
425
+ const query = new Query({
426
+ driver: this.connection(),
427
+ handler,
428
+ modelClass: this
429
+ })
430
+
431
+ return query.from(new FromTable({driver: this.connection(), tableName: this.tableName()}))
432
+ }
433
+
434
+ static orderableColumn() {
435
+ // FIXME: Allow to change to 'created_at' if using UUID?
436
+
437
+ return this.primaryKey()
438
+ }
439
+
440
+ static all() {
441
+ return this._newQuery()
442
+ }
443
+
444
+ static async findBy(...args) {
445
+ return this._newQuery().findBy(...args)
446
+ }
447
+
448
+ static async findOrCreateBy(...args) {
449
+ return this._newQuery().findOrCreateBy(...args)
450
+ }
451
+
452
+ static async findOrInitializeBy(...args) {
453
+ return this._newQuery().findOrInitializeBy(...args)
454
+ }
455
+
456
+ static joins(...args) {
457
+ return this._newQuery().joins(...args)
458
+ }
459
+
460
+ static preload(...args) {
461
+ return this._newQuery().preload(...args)
462
+ }
463
+
464
+ static where(...args) {
465
+ return this._newQuery().where(...args)
466
+ }
467
+
468
+ constructor(changes = {}) {
469
+ this._attributes = {}
470
+ this._changes = {}
471
+ this._isNewRecord = true
472
+ this._relationships = {}
473
+
474
+ for (const key in changes) {
475
+ this.setAttribute(key, changes[key])
476
+ }
477
+ }
478
+
479
+ loadExistingRecord(attributes) {
480
+ this._attributes = attributes
481
+ this._isNewRecord = false
482
+ }
483
+
484
+ assign(attributesToAssign) {
485
+ for (const attributeToAssign in attributesToAssign) {
486
+ this.setAttribute(attributeToAssign, attributesToAssign[attributeToAssign])
487
+ }
488
+ }
489
+
490
+ attributes() {
491
+ return Object.assign({}, this._attributes, this._changes)
492
+ }
493
+
494
+ _connection() {
495
+ if (this.__connection) return this.__connection
496
+
497
+ return this.constructor.connection()
498
+ }
499
+
500
+ async destroy() {
501
+ const conditions = {}
502
+
503
+ conditions[this.constructor.primaryKey()] = this.id()
504
+
505
+ const sql = this._connection().deleteSql({
506
+ conditions,
507
+ tableName: this._tableName()
508
+ })
509
+
510
+ await this._connection().query(sql)
511
+ }
512
+
513
+ _hasChanges = () => Object.keys(this._changes).length > 0
514
+
515
+ isChanged() {
516
+ if (this.isNewRecord() || this._hasChanges()) {
517
+ return true
518
+ }
519
+
520
+ // Check if a loaded sub-model of a relationship is changed and should be saved along with this model.
521
+ if (this._instanceRelationships) {
522
+ for (const instanceRelationshipName in this._instanceRelationships) {
523
+ const instanceRelationship = this._instanceRelationships[instanceRelationshipName]
524
+ let loaded = instanceRelationship._loaded
525
+
526
+ if (!loaded) continue
527
+ if (!Array.isArray(loaded)) loaded = [loaded]
528
+
529
+ for (const model of loaded) {
530
+ if (model.isChanged()) {
531
+ return true
532
+ }
533
+ }
534
+ }
535
+ }
536
+
537
+ return false
538
+ }
539
+
540
+ _tableName() {
541
+ if (this.__tableName) return this.__tableName
542
+
543
+ return this.constructor.tableName()
544
+ }
545
+
546
+ readAttribute(attributeName) {
547
+ const attributeNameUnderscore = inflection.underscore(attributeName)
548
+
549
+ if (attributeNameUnderscore in this._changes) return this._changes[attributeNameUnderscore]
550
+
551
+ if (!(attributeNameUnderscore in this._attributes) && this.isPersisted()) {
552
+ throw new Error(`No such attribute or not selected ${this.constructor.name}#${attributeName}`)
553
+ }
554
+
555
+ return this._attributes[attributeNameUnderscore]
556
+ }
557
+
558
+ async _createNewRecord() {
559
+ if (!this.constructor.connection()["insertSql"]) {
560
+ throw new Error(`No insertSql on ${this.constructor.connection().constructor.name}`)
561
+ }
562
+
563
+ const sql = this._connection().insertSql({
564
+ tableName: this._tableName(),
565
+ data: this.attributes()
566
+ })
567
+ await this._connection().query(sql)
568
+ const id = await this._connection().lastInsertID()
569
+
570
+ await this._reloadWithId(id)
571
+ this.setIsNewRecord(false)
572
+ }
573
+
574
+ async _updateRecordWithChanges() {
575
+ if (!this._hasChanges()) return
576
+
577
+ const conditions = {}
578
+
579
+ conditions[this.constructor.primaryKey()] = this.id()
580
+
581
+ const sql = this._connection().updateSql({
582
+ tableName: this._tableName(),
583
+ data: this._changes,
584
+ conditions
585
+ })
586
+ await this._connection().query(sql)
587
+ await this._reloadWithId(this.id())
588
+ }
589
+
590
+ id = () => this.readAttribute(this.constructor.primaryKey())
591
+ isPersisted = () => !this._isNewRecord
592
+ isNewRecord = () => this._isNewRecord
593
+
594
+ setIsNewRecord(newIsNewRecord) {
595
+ this._isNewRecord = newIsNewRecord
596
+ }
597
+
598
+ async _reloadWithId(id) {
599
+ const primaryKey = this.constructor.primaryKey()
600
+ const whereObject = {}
601
+
602
+ whereObject[primaryKey] = id
603
+
604
+ const query = this.constructor.where(whereObject)
605
+ const reloadedModel = await query.first()
606
+
607
+ if (!reloadedModel) throw new Error(`${this.constructor.name}#${this.id()} couldn't be reloaded - record didn't exist`)
608
+
609
+ this._attributes = reloadedModel.attributes()
610
+ this._changes = {}
611
+ }
612
+
613
+ async reload() {
614
+ this._reloadWithId(this.readAttribute("id"))
615
+ }
616
+
617
+ async update(attributesToAssign) {
618
+ if (attributesToAssign) this.assign(attributesToAssign)
619
+
620
+ await this.save()
621
+ }
622
+ }
@@ -0,0 +1,28 @@
1
+ export default class VelociousDatabaseRecordBaseInstanceRelationship {
2
+ constructor({model, relationship}) {
3
+ this.model = model
4
+ this.relationship = relationship
5
+ }
6
+
7
+ loaded() {
8
+ if (!this._preloaded && this.model.isPersisted()) {
9
+ throw new Error(`${this.model.constructor.name}#${this.relationship.getRelationshipName()} hasn't been preloaded`)
10
+ }
11
+
12
+ return this._loaded
13
+ }
14
+
15
+ setLoaded(model) {
16
+ this._loaded = model
17
+ }
18
+
19
+ setPreloaded(preloadedValue) {
20
+ this._preloaded = preloadedValue
21
+ }
22
+
23
+ getForeignKey = () => this.getRelationship().getForeignKey()
24
+ getPrimaryKey = () => this.getRelationship().getPrimaryKey()
25
+ getRelationship = () => this.relationship
26
+ getTargetModelClass = () => this.getRelationship().getTargetModelClass()
27
+ getType = () => this.getRelationship().getType()
28
+ }