odac 0.9.0

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 (213) hide show
  1. package/.editorconfig +21 -0
  2. package/.github/workflows/auto-pr-description.yml +49 -0
  3. package/.github/workflows/release.yml +32 -0
  4. package/.github/workflows/test-coverage.yml +58 -0
  5. package/.husky/pre-commit +2 -0
  6. package/.kiro/steering/code-style.md +56 -0
  7. package/.kiro/steering/product.md +20 -0
  8. package/.kiro/steering/structure.md +77 -0
  9. package/.kiro/steering/tech.md +87 -0
  10. package/.prettierrc +10 -0
  11. package/.releaserc.js +134 -0
  12. package/AGENTS.md +84 -0
  13. package/CHANGELOG.md +181 -0
  14. package/CODE_OF_CONDUCT.md +83 -0
  15. package/CONTRIBUTING.md +63 -0
  16. package/LICENSE +661 -0
  17. package/README.md +57 -0
  18. package/SECURITY.md +26 -0
  19. package/bin/candy +10 -0
  20. package/bin/candypack +10 -0
  21. package/cli/index.js +3 -0
  22. package/cli/src/Cli.js +348 -0
  23. package/cli/src/Connector.js +93 -0
  24. package/cli/src/Monitor.js +416 -0
  25. package/core/Candy.js +87 -0
  26. package/core/Commands.js +239 -0
  27. package/core/Config.js +1094 -0
  28. package/core/Lang.js +52 -0
  29. package/core/Log.js +43 -0
  30. package/core/Process.js +26 -0
  31. package/docs/backend/01-overview/01-whats-in-the-candy-box.md +9 -0
  32. package/docs/backend/01-overview/02-super-handy-helper-functions.md +9 -0
  33. package/docs/backend/01-overview/03-development-server.md +79 -0
  34. package/docs/backend/02-structure/01-typical-project-layout.md +39 -0
  35. package/docs/backend/03-config/00-configuration-overview.md +214 -0
  36. package/docs/backend/03-config/01-database-connection.md +60 -0
  37. package/docs/backend/03-config/02-static-route-mapping-optional.md +20 -0
  38. package/docs/backend/03-config/03-request-timeout.md +11 -0
  39. package/docs/backend/03-config/04-environment-variables.md +227 -0
  40. package/docs/backend/03-config/05-early-hints.md +352 -0
  41. package/docs/backend/04-routing/01-basic-page-routes.md +28 -0
  42. package/docs/backend/04-routing/02-controller-less-view-routes.md +43 -0
  43. package/docs/backend/04-routing/03-api-and-data-routes.md +20 -0
  44. package/docs/backend/04-routing/04-authentication-aware-routes.md +48 -0
  45. package/docs/backend/04-routing/05-advanced-routing.md +14 -0
  46. package/docs/backend/04-routing/06-error-pages.md +101 -0
  47. package/docs/backend/04-routing/07-cron-jobs.md +149 -0
  48. package/docs/backend/05-controllers/01-how-to-build-a-controller.md +17 -0
  49. package/docs/backend/05-controllers/02-your-trusty-candy-assistant.md +20 -0
  50. package/docs/backend/05-controllers/03-controller-classes.md +93 -0
  51. package/docs/backend/05-forms/01-custom-forms.md +395 -0
  52. package/docs/backend/05-forms/02-automatic-database-insert.md +297 -0
  53. package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +96 -0
  54. package/docs/backend/06-request-and-response/02-sending-a-response-replying-to-the-user.md +40 -0
  55. package/docs/backend/07-views/01-the-view-directory.md +73 -0
  56. package/docs/backend/07-views/02-rendering-a-view.md +179 -0
  57. package/docs/backend/07-views/03-template-syntax.md +181 -0
  58. package/docs/backend/07-views/03-variables.md +328 -0
  59. package/docs/backend/07-views/04-request-data.md +231 -0
  60. package/docs/backend/07-views/05-conditionals.md +290 -0
  61. package/docs/backend/07-views/06-loops.md +353 -0
  62. package/docs/backend/07-views/07-translations.md +358 -0
  63. package/docs/backend/07-views/08-backend-javascript.md +398 -0
  64. package/docs/backend/07-views/09-comments.md +297 -0
  65. package/docs/backend/08-database/01-database-connection.md +99 -0
  66. package/docs/backend/08-database/02-using-mysql.md +322 -0
  67. package/docs/backend/09-validation/01-the-validator-service.md +424 -0
  68. package/docs/backend/10-authentication/01-user-logins-with-authjs.md +53 -0
  69. package/docs/backend/10-authentication/02-foiling-villains-with-csrf-protection.md +55 -0
  70. package/docs/backend/10-authentication/03-register.md +134 -0
  71. package/docs/backend/10-authentication/04-candy-register-forms.md +676 -0
  72. package/docs/backend/10-authentication/05-session-management.md +159 -0
  73. package/docs/backend/10-authentication/06-candy-login-forms.md +596 -0
  74. package/docs/backend/11-mail/01-the-mail-service.md +42 -0
  75. package/docs/backend/12-streaming/01-streaming-overview.md +300 -0
  76. package/docs/backend/13-utilities/01-candy-var.md +504 -0
  77. package/docs/frontend/01-overview/01-introduction.md +146 -0
  78. package/docs/frontend/02-ajax-navigation/01-quick-start.md +608 -0
  79. package/docs/frontend/02-ajax-navigation/02-configuration.md +370 -0
  80. package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +519 -0
  81. package/docs/frontend/03-forms/01-form-handling.md +420 -0
  82. package/docs/frontend/04-api-requests/01-get-post.md +443 -0
  83. package/docs/frontend/05-streaming/01-client-streaming.md +163 -0
  84. package/docs/index.json +452 -0
  85. package/docs/server/01-installation/01-quick-install.md +19 -0
  86. package/docs/server/01-installation/02-manual-installation-via-npm.md +9 -0
  87. package/docs/server/02-get-started/01-core-concepts.md +7 -0
  88. package/docs/server/02-get-started/02-basic-commands.md +57 -0
  89. package/docs/server/02-get-started/03-cli-reference.md +276 -0
  90. package/docs/server/02-get-started/04-cli-quick-reference.md +102 -0
  91. package/docs/server/03-service/01-start-a-new-service.md +57 -0
  92. package/docs/server/03-service/02-delete-a-service.md +48 -0
  93. package/docs/server/04-web/01-create-a-website.md +36 -0
  94. package/docs/server/04-web/02-list-websites.md +9 -0
  95. package/docs/server/04-web/03-delete-a-website.md +29 -0
  96. package/docs/server/05-subdomain/01-create-a-subdomain.md +32 -0
  97. package/docs/server/05-subdomain/02-list-subdomains.md +33 -0
  98. package/docs/server/05-subdomain/03-delete-a-subdomain.md +41 -0
  99. package/docs/server/06-ssl/01-renew-an-ssl-certificate.md +34 -0
  100. package/docs/server/07-mail/01-create-a-mail-account.md +23 -0
  101. package/docs/server/07-mail/02-delete-a-mail-account.md +20 -0
  102. package/docs/server/07-mail/03-list-mail-accounts.md +20 -0
  103. package/docs/server/07-mail/04-change-account-password.md +23 -0
  104. package/eslint.config.mjs +120 -0
  105. package/framework/index.js +4 -0
  106. package/framework/src/Auth.js +309 -0
  107. package/framework/src/Candy.js +81 -0
  108. package/framework/src/Config.js +79 -0
  109. package/framework/src/Env.js +60 -0
  110. package/framework/src/Lang.js +57 -0
  111. package/framework/src/Mail.js +83 -0
  112. package/framework/src/Mysql.js +575 -0
  113. package/framework/src/Request.js +301 -0
  114. package/framework/src/Route/Cron.js +128 -0
  115. package/framework/src/Route/Internal.js +439 -0
  116. package/framework/src/Route.js +455 -0
  117. package/framework/src/Server.js +15 -0
  118. package/framework/src/Stream.js +163 -0
  119. package/framework/src/Token.js +37 -0
  120. package/framework/src/Validator.js +271 -0
  121. package/framework/src/Var.js +211 -0
  122. package/framework/src/View/EarlyHints.js +190 -0
  123. package/framework/src/View/Form.js +600 -0
  124. package/framework/src/View.js +513 -0
  125. package/framework/web/candy.js +838 -0
  126. package/jest.config.js +22 -0
  127. package/locale/de-DE.json +80 -0
  128. package/locale/en-US.json +79 -0
  129. package/locale/es-ES.json +80 -0
  130. package/locale/fr-FR.json +80 -0
  131. package/locale/pt-BR.json +80 -0
  132. package/locale/ru-RU.json +80 -0
  133. package/locale/tr-TR.json +85 -0
  134. package/locale/zh-CN.json +80 -0
  135. package/package.json +86 -0
  136. package/server/index.js +5 -0
  137. package/server/src/Api.js +88 -0
  138. package/server/src/DNS.js +940 -0
  139. package/server/src/Hub.js +535 -0
  140. package/server/src/Mail.js +571 -0
  141. package/server/src/SSL.js +180 -0
  142. package/server/src/Server.js +27 -0
  143. package/server/src/Service.js +248 -0
  144. package/server/src/Subdomain.js +64 -0
  145. package/server/src/Web/Firewall.js +170 -0
  146. package/server/src/Web/Proxy.js +134 -0
  147. package/server/src/Web.js +451 -0
  148. package/server/src/mail/imap.js +1091 -0
  149. package/server/src/mail/server.js +32 -0
  150. package/server/src/mail/smtp.js +786 -0
  151. package/test/cli/Cli.test.js +36 -0
  152. package/test/core/Candy.test.js +234 -0
  153. package/test/core/Commands.test.js +538 -0
  154. package/test/core/Config.test.js +1435 -0
  155. package/test/core/Lang.test.js +250 -0
  156. package/test/core/Process.test.js +156 -0
  157. package/test/framework/Route.test.js +239 -0
  158. package/test/framework/View/EarlyHints.test.js +282 -0
  159. package/test/scripts/check-coverage.js +132 -0
  160. package/test/server/Api.test.js +647 -0
  161. package/test/server/Client.test.js +338 -0
  162. package/test/server/DNS.test.js +2050 -0
  163. package/test/server/DNS.test.js.bak +2084 -0
  164. package/test/server/Log.test.js +73 -0
  165. package/test/server/Mail.account.test_.js +460 -0
  166. package/test/server/Mail.init.test_.js +411 -0
  167. package/test/server/Mail.test_.js +1340 -0
  168. package/test/server/SSL.test_.js +1491 -0
  169. package/test/server/Server.test.js +765 -0
  170. package/test/server/Service.test_.js +1127 -0
  171. package/test/server/Subdomain.test.js +440 -0
  172. package/test/server/Web/Firewall.test.js +175 -0
  173. package/test/server/Web.test_.js +1562 -0
  174. package/test/server/__mocks__/acme-client.js +17 -0
  175. package/test/server/__mocks__/bcrypt.js +50 -0
  176. package/test/server/__mocks__/child_process.js +389 -0
  177. package/test/server/__mocks__/crypto.js +432 -0
  178. package/test/server/__mocks__/fs.js +450 -0
  179. package/test/server/__mocks__/globalCandy.js +227 -0
  180. package/test/server/__mocks__/http-proxy.js +105 -0
  181. package/test/server/__mocks__/http.js +575 -0
  182. package/test/server/__mocks__/https.js +272 -0
  183. package/test/server/__mocks__/index.js +249 -0
  184. package/test/server/__mocks__/mail/server.js +100 -0
  185. package/test/server/__mocks__/mail/smtp.js +31 -0
  186. package/test/server/__mocks__/mailparser.js +81 -0
  187. package/test/server/__mocks__/net.js +369 -0
  188. package/test/server/__mocks__/node-forge.js +328 -0
  189. package/test/server/__mocks__/os.js +320 -0
  190. package/test/server/__mocks__/path.js +291 -0
  191. package/test/server/__mocks__/selfsigned.js +8 -0
  192. package/test/server/__mocks__/server/src/mail/server.js +100 -0
  193. package/test/server/__mocks__/server/src/mail/smtp.js +31 -0
  194. package/test/server/__mocks__/smtp-server.js +106 -0
  195. package/test/server/__mocks__/sqlite3.js +394 -0
  196. package/test/server/__mocks__/testFactories.js +299 -0
  197. package/test/server/__mocks__/testHelpers.js +363 -0
  198. package/test/server/__mocks__/tls.js +229 -0
  199. package/watchdog/index.js +3 -0
  200. package/watchdog/src/Watchdog.js +156 -0
  201. package/web/config.json +5 -0
  202. package/web/controller/page/about.js +27 -0
  203. package/web/controller/page/index.js +34 -0
  204. package/web/package.json +18 -0
  205. package/web/public/assets/css/style.css +1835 -0
  206. package/web/public/assets/js/app.js +96 -0
  207. package/web/route/www.js +19 -0
  208. package/web/skeleton/main.html +22 -0
  209. package/web/view/content/about.html +65 -0
  210. package/web/view/content/home.html +205 -0
  211. package/web/view/footer/main.html +11 -0
  212. package/web/view/head/main.html +5 -0
  213. package/web/view/header/main.html +14 -0
@@ -0,0 +1,575 @@
1
+ 'use strict'
2
+ const mysql = require('mysql2')
3
+
4
+ class Raw {
5
+ constructor(value) {
6
+ this.value = value
7
+ }
8
+ }
9
+
10
+ class Mysql {
11
+ #conn
12
+ #database
13
+ #defining = false
14
+ #table = []
15
+ #arr = {}
16
+ #stack = []
17
+ #statements = ['=', '>', '>=', '<', '<=', '!=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN', 'IS', 'IS NOT']
18
+
19
+ constructor(table, conn) {
20
+ this.#conn = conn
21
+ this.#stack = new Error().stack.split('\n').splice(3)
22
+ if (table) {
23
+ this.#arr.table = table
24
+ this.#define(table)
25
+ this.#defining = 1
26
+ while (this.#defining && this.#defining < 255) {
27
+ this.#defining++
28
+ }
29
+ }
30
+ }
31
+
32
+ // function having(){
33
+ // if(count(func_get_args()) == 1 && !is_array(func_get_args()[0])){
34
+ // $this->arr['having'] = is_numeric(func_get_args()[0]) ? "id='".func_get_args()[0]."'" : "";
35
+ // }elseif(count(func_get_args()) > 0){
36
+ // $this->arr['having'] = isset($this->arr['having']) && trim($this->arr['having'])!='' ? $this->arr['having'].' AND '.$this->whereExtract(func_get_args()) : $this->whereExtract(func_get_args());
37
+ // }
38
+ // return new static($this->table,$this->arr);
39
+ // }
40
+
41
+ // function whereJson($col,$val){
42
+ // //return 'JSON_SEARCH('.$col.', "one", "'.$val.'") IS NOT NULL';
43
+ // return new static($this->table,$this->arr);
44
+ // }
45
+ // function cache($t=3600){
46
+ // if(!is_numeric($t)){
47
+ // $exp = explode(' ',str_replace(' ',' ',$t));
48
+ // if($exp[1] == 'second') $t = intval(trim($exp[0]));
49
+ // if($exp[1] == 'minute') $t = intval(trim($exp[0])) * 60;
50
+ // if($exp[1] == 'hour') $t = intval(trim($exp[0])) * 60 * 60;
51
+ // if($exp[1] == 'day') $t = intval(trim($exp[0])) * 60 * 60 * 24;
52
+ // if($exp[1] == 'week') $t = intval(trim($exp[0])) * 60 * 60 * 24 * 7;
53
+ // if($exp[1] == 'month') $t = intval(trim($exp[0])) * 60 * 60 * 24 * 30;
54
+ // if($exp[1] == 'year') $t = intval(trim($exp[0])) * 60 * 60 * 24 * 365;
55
+ // }
56
+ // $this->arr['cache'] = $t;
57
+ // return new static($this->table,$this->arr);
58
+ // }
59
+
60
+ async #define(table) {
61
+ return new Promise(resolve => {
62
+ if (!Candy.Mysql.db[this.#database]) Candy.Mysql.db[this.#database] = {}
63
+ this.#table[table] = Candy.Mysql.db[this.#database][table]
64
+ if (this.#table[table]) {
65
+ this.#defining = false
66
+ return resolve(true)
67
+ }
68
+ let columns = []
69
+ this.#conn.query(`SHOW COLUMNS FROM ${this.escape(table, 'table')}`, (err, result) => {
70
+ if (err) {
71
+ this.#error(err)
72
+ this.#defining = false
73
+ return resolve(false)
74
+ }
75
+ for (let get of result) {
76
+ columns[get.Field] = get
77
+ if (get.Key == 'PRI') {
78
+ if (!this.#table[table]) this.#table[table] = {}
79
+ this.#table[table].primary = get.Field
80
+ }
81
+ }
82
+ if (!this.#table[table]) this.#table[table] = {}
83
+ this.#table[table].columns = columns
84
+ Candy.Mysql.db[this.#database][table] = this.#table[table]
85
+ this.#defining = false
86
+ return resolve(true)
87
+ })
88
+ })
89
+ }
90
+
91
+ async delete() {
92
+ let query = this.query('delete')
93
+ let run = await this.run(query)
94
+ if (run === false) return false
95
+ this.affected = run.affectedRows
96
+ // if($this->affected > 0) self::clearcache();
97
+ return this
98
+ }
99
+
100
+ #error(err, query) {
101
+ err = 'CandyPack Mysql Error: ' + (err?.message ?? 'Unknown error').trim() + '\n'
102
+ if (query) err += 'Query: ' + query + '\n'
103
+ while (this.#stack.length > 0) {
104
+ let line = this.#stack.shift().replace('at', '')
105
+ if (line.includes('/node_modules/candypack/framework/src/')) break
106
+ else if (!line.includes('(node:')) err += line + '\n'
107
+ }
108
+ console.error(err)
109
+ return false
110
+ }
111
+
112
+ escape(v, type) {
113
+ if (!type) type = 'value'
114
+ if (v && v instanceof Raw) return ' ' + v.value + ' '
115
+ if (type == 'value') {
116
+ if (v === null) return 'NULL'
117
+ if (typeof v === 'object')
118
+ return (
119
+ ' (' +
120
+ Object.values(v)
121
+ .map(val => mysql.escape(val))
122
+ .join(',') +
123
+ ') '
124
+ )
125
+ return `${mysql.escape(v)}`
126
+ } else if (type == 'table' || type == 'col') {
127
+ let as = ''
128
+ if (typeof v === 'object') {
129
+ as = Object.values(v)[0]
130
+ v = Object.keys(v)[0]
131
+ as = type == 'col' ? ` AS ${mysql.escapeId(as)} ` : ` ${mysql.escapeId(as)} `
132
+ }
133
+ if (v.includes('.')) {
134
+ return (
135
+ v
136
+ .split('.')
137
+ .map(val => mysql.escapeId(val))
138
+ .join('.') + as
139
+ )
140
+ }
141
+ return mysql.escapeId(v) + as
142
+ } else if (type == 'statement' || type == 'st') {
143
+ return this.#statements.includes(v.toUpperCase()) ? v.toUpperCase() : '='
144
+ }
145
+ }
146
+
147
+ first(b = false) {
148
+ return new Promise((resolve, reject) => {
149
+ this.#arr.limit = 1
150
+ this.get(b)
151
+ .then(sql => {
152
+ if (sql === false || !sql[0]) return resolve(false)
153
+ return resolve(sql[0])
154
+ })
155
+ .catch(reject)
156
+ })
157
+ }
158
+
159
+ async get(b) {
160
+ if (!b) b = false
161
+ let data = []
162
+ // if(isset($this->arr['cache'])){
163
+ // $md5_query = md5($query);
164
+ // $md5_table = md5($this->arr['table']);
165
+ // $file = "cache/mysql/".md5(Mysql::$name)."/$md5_table"."_$md5_query";
166
+ // $cache = Candy::storage($file)->get('cache');
167
+ // if(isset($cache->date) && ($cache->date >= (time() - $this->arr['cache']))) return $cache->data;
168
+ // }
169
+ let query = this.query('get')
170
+ let sql = await this.run(query)
171
+ // console.log(sql);
172
+ if (sql === false) return this.#error()
173
+ for (let row of sql) {
174
+ for (let [key, value] of Object.entries(row)) row[key] = await this.type(key, value)
175
+ data.push(row)
176
+ }
177
+ // while($row = mysqli_fetch_assoc($sql)){
178
+ // foreach($row as $key => $value) $row[$key] = $this->type($key, $value);
179
+ // $data[] = $b ? $row : (object)$row;
180
+ // }
181
+ // mysqli_free_result($sql);
182
+ // if(isset($cache)){
183
+ // $cache->data = $data;
184
+ // $cache->date = time();
185
+ // Candy::storage($file)->set('cache', $cache);
186
+ // }
187
+ return data
188
+ }
189
+
190
+ async insert(arr) {
191
+ this.id = 1
192
+ let ext = await this.#valuesExtract(arr)
193
+ this.#arr['into'] = ext['into']
194
+ this.#arr['values'] = ext['values']
195
+ let query = this.query('insert')
196
+ let run = await this.run(query)
197
+ if (run === false) return false
198
+ this.id = run.insertId
199
+ this.affected = run.affectedRows
200
+ // if(this.affected > 0) this.clearcache();
201
+ return this
202
+ }
203
+
204
+ insertIgnore(arr) {
205
+ this.#arr.ignore = true
206
+ return this.insert(arr)
207
+ }
208
+
209
+ order(v1, v2 = 'asc') {
210
+ // if(is_array($v1) && (!isset($v1['ct']) || $v1['ct'] != $GLOBALS['candy_token_mysql'])){
211
+ // $order = [];
212
+ // foreach($v1 as $key => $val)
213
+ // if(!is_int($key)) $order[] = $this->escape($key,'col').(strtolower($val) == 'desc' ? ' DESC' : ' ASC');
214
+ // else $order[] = $this->escape($val,'col').' ASC';
215
+ // $this->arr['order by'] = implode(',',$order);
216
+ /* }else */ this.#arr['order by'] = this.escape(v1, 'col') + (v2.toLowerCase() == 'desc' ? ' DESC' : ' ASC')
217
+ return this
218
+ }
219
+
220
+ orWhere(...args) {
221
+ this.#arr.where =
222
+ this.#arr.where && this.#arr.where.trim() != '' ? `${this.#arr.where} OR ${this.#whereExtract(args)}` : this.#whereExtract(args)
223
+ return this
224
+ }
225
+
226
+ async replace(arr) {
227
+ // $this->id = 1;
228
+ let ext = await this.#valuesExtract(arr)
229
+ this.#arr['into'] = ext['into']
230
+ this.#arr['values'] = ext['values']
231
+ let query = this.query('replace')
232
+ let run = await this.run(query)
233
+ if (run === false) return false
234
+ this.id = run.insertId
235
+ this.affected = run.affectedRows
236
+ // if($sql === false) return $this->error();
237
+ // $this->success = $sql;
238
+ // self::clearcache();
239
+ return this
240
+ }
241
+
242
+ async rows() {
243
+ // if(isset($this->arr['cache'])){
244
+ // $md5_query = md5($query);
245
+ // $md5_table = md5($this->arr['table']);
246
+ // $file = "cache/mysql/".md5(Mysql::$name)."/$md5_table"."_$md5_query"."_r";
247
+ // $cache = Candy::storage($file)->get('cache');
248
+ // if(isset($cache->date) && ($cache->date >= (time() - $this->arr['cache']))) return $cache->data;
249
+ // }
250
+ let query = this.query('get')
251
+ let sql = await this.run(query)
252
+ if (sql === false) return this.#error()
253
+ let rows = sql.length
254
+ // if(isset($cache)){
255
+ // $cache->data = $rows;
256
+ // $cache->date = time();
257
+ // Candy::storage($file)->set('cache', $cache);
258
+ // }
259
+ return rows
260
+ }
261
+
262
+ run(query, params) {
263
+ return new Promise(resolve => {
264
+ if (!query) return resolve(false)
265
+ if (!this.#conn) return resolve(false)
266
+ if (this.#conn.state == 'disconnected') Candy.Mysql.init()
267
+ const args = params ? [query, params] : [query]
268
+ args.push((err, result) => {
269
+ if (err) return resolve(this.#error(err, query))
270
+ return resolve(result)
271
+ })
272
+ this.#conn.query(...args)
273
+ })
274
+ }
275
+
276
+ select(...args) {
277
+ this.#arr['select'] = this.#arr['select'] ?? []
278
+ if (args.length == 1 && (typeof args[0] === 'object' || args[0] instanceof Raw)) {
279
+ if (args[0] instanceof Raw) {
280
+ this.#arr['select'].push(args[0].value)
281
+ } else {
282
+ for (let key of Object.keys(args[0])) {
283
+ let value = args[0][key]
284
+ if (isNaN(key)) this.#arr['select'].push(this.escape(key, 'col') + ' AS ' + this.escape(value, 'col'))
285
+ else this.#arr['select'].push(this.escape(value, 'col'))
286
+ }
287
+ }
288
+ } else {
289
+ for (let key of args) this.#arr['select'].push(this.escape(key, 'col'))
290
+ }
291
+ return this
292
+ }
293
+
294
+ async set(arr, val) {
295
+ let vars = ''
296
+ if (!['array', 'object'].includes(typeof arr) && val !== undefined)
297
+ vars += this.escape(arr, 'col') + ' = ' + this.escape(await this.type(arr, val, 'encode')) + ','
298
+ else
299
+ for (let [key, value] of Object.entries(arr))
300
+ vars += this.escape(key, 'col') + ' = ' + this.escape(await this.type(key, value, 'encode')) + ','
301
+ this.#arr.set = vars.substring(0, vars.length - 1)
302
+ let query = this.query('set')
303
+ let run = await this.run(query)
304
+ if (run === false) return this.#error()
305
+ this.affected = run.affectedRows
306
+ if (this.affected > 0) this.#clearcache()
307
+ return this
308
+ }
309
+
310
+ groupBy(...args) {
311
+ this.#arr['group by'] = this.#arr['group by'] ?? ''
312
+ let select = this.#arr['group by'].split(',')
313
+ // if(count(func_get_args())==1 && is_array(func_get_args()[0])){
314
+ // if(isset(func_get_args()[0]['ct']) && isset(func_get_args()[0]['v']) && func_get_args()[0]['ct'] == $GLOBALS['candy_token_mysql']){
315
+ // $select[] = func_get_args()[0]['v'];
316
+ // }else{
317
+ // foreach(func_get_args()[0] as $key => $value){
318
+ // $select[] = $this->escape($value,'col');
319
+ // }
320
+ // }
321
+ /* } else */ for (let key of args) select.push(this.escape(key, 'col'))
322
+ this.#arr['group by'] = select.join(', ')
323
+ return this
324
+ }
325
+
326
+ limit(v1, v2 = null) {
327
+ this.#arr['limit'] = v2 === null ? v1 : `${v1}, ${v2}`
328
+ return this
329
+ }
330
+
331
+ leftJoin(tb, col1, st = null, col2 = null) {
332
+ return this.join(tb, col1, st, col2, 'left join')
333
+ }
334
+
335
+ rightJoin(tb, col1, st = null, col2 = null) {
336
+ return this.join(tb, col1, st, col2, 'right join')
337
+ }
338
+
339
+ join(tb, col1, st = null, col2 = null, type = 'inner join') {
340
+ this.#arr[type] = this.#arr[type] ?? []
341
+ this.#define(Array.isArray(tb) ? Object.keys(tb)[0] : tb)
342
+ tb = this.escape(tb, 'table')
343
+ let state
344
+ if (st === null && col2 === null) {
345
+ col1 = this.#whereExtract(col1)
346
+ col2 = ''
347
+ state = ''
348
+ } else {
349
+ col1 = this.escape(col1, 'col')
350
+ col2 = this.escape(col2 !== null ? col2 : st, 'col')
351
+ state = this.escape(col2 !== null ? st : '=', 'st')
352
+ }
353
+ this.#arr[type].push(`${tb} ON ${col1} ${state} ${col2}`)
354
+ return this
355
+ }
356
+
357
+ #clearcache() {
358
+ // if(!isset($this->arr['table'])) return false;
359
+ // $md5_table = md5($this->arr['table']);
360
+ // $file = "storage/cache/mysql/".md5(Mysql::$name)."/$md5_table*";
361
+ // foreach(glob($file) as $key) unlink($key);
362
+ return true
363
+ }
364
+
365
+ query(type = 'get') {
366
+ const arr_q = ['inner join', 'right join', 'left join', 'where', 'group by', 'having', 'order by', 'limit']
367
+ let query = ''
368
+ for (let key of arr_q) {
369
+ if (this.#arr[key]) {
370
+ if (Array.isArray(this.#arr[key])) {
371
+ query += ' ' + key.toUpperCase() + ' ' + this.#arr[key].join(' ' + key.toUpperCase() + ' ')
372
+ } else {
373
+ query += ' ' + key.toUpperCase() + ' '
374
+ query += this.#arr[key]
375
+ }
376
+ }
377
+ }
378
+ switch (type) {
379
+ case 'get':
380
+ query = `SELECT ${this.#arr.select ? this.#arr.select.join(', ') : '*'} FROM ${this.escape(this.#arr.table, 'table')} ${query}`
381
+ break
382
+ case 'set':
383
+ query = `UPDATE ${this.escape(this.#arr['table'], 'table')} SET ${this.#arr['set']} ${query}`
384
+ break
385
+ case 'insert':
386
+ query = `INSERT ${this.#arr.ignore ? 'IGNORE' : ''} INTO ${this.escape(this.#arr.table, 'table')} ${this.#arr.into} VALUES ${this.#arr.values}`
387
+ break
388
+ case 'delete':
389
+ query = `DELETE FROM ${this.escape(this.#arr.table, 'table')} ${query}`
390
+ break
391
+ case 'replace':
392
+ query = `REPLACE INTO ${this.escape(this.#arr.table, 'table')} ${this.#arr.into} VALUES ${this.#arr.values}`
393
+ break
394
+ }
395
+ return query
396
+ }
397
+
398
+ async type(col, value, action = 'decode') {
399
+ if (!this.types) this.types = {}
400
+ if (!this.types[col]) {
401
+ this.types[col] = 'string'
402
+ for (const key of Object.keys(this.#table)) {
403
+ if (!this.#table[key]) await this.#define(key)
404
+ if (!this.#table[key]) throw new Error(`Table ${key} not found`)
405
+ if (!this.#arr.select && this.#table[key].columns[col]?.Type) {
406
+ this.types[col] = this.#table[key].columns[col]?.Type ?? this.types[col]
407
+ break
408
+ } else if (!this.#arr.select) {
409
+ continue
410
+ } else if (Candy.Var(this.#arr.select).contains(' AS "' + col + '"')) {
411
+ // $exp = explode(' ,',explode(" AS \"$col\"",$this->arr['select'])[0]);
412
+ // $real_col = explode('.',Candy::var(trim(end($exp)))->clear('`'));
413
+ // $real_table = trim($real_col[0]);
414
+ // $real_col = trim($real_col[1]);
415
+ // $this->types[$col] = $this->types[$col] = $this->table[$real_table]['columns'][$real_col]['Type'] ?? $this->types[$col];
416
+ break
417
+ } else if (Candy.Var(this.#arr.select).containsAny(' `' + col + '`', ' `' + key + '`.`' + col + '`')) {
418
+ this.types[col] = this.#table[key].columns[col].Type ?? this.types[col]
419
+ }
420
+ }
421
+ }
422
+ if (action == 'decode') {
423
+ if (Candy.Var(this.types[col]).isBegin('tinyint(1)')) value = value ? true : false
424
+ else if (Candy.Var(this.types[col]).isBegin('int')) value = parseInt(value)
425
+ else if (Candy.Var(this.types[col]).isBegin('double')) value = parseFloat(value)
426
+ else if (Candy.Var(this.types[col]).isBegin('float')) value = parseFloat(value)
427
+ else if (Candy.Var(this.types[col]).isBegin('boolean')) value = parseInt(value)
428
+ else if (Candy.Var(this.types[col]).isBegin('json')) value = JSON.parse(value)
429
+ } else if (!(value instanceof Raw)) {
430
+ if (Candy.Var(this.types[col]).isBegin('tinyint(1)')) value = parseInt(value)
431
+ else if (Candy.Var(this.types[col]).isBegin('int')) value = parseInt(value)
432
+ else if (Candy.Var(this.types[col]).isBegin('double')) value = parseFloat(value)
433
+ else if (Candy.Var(this.types[col]).isBegin('float')) value = parseFloat(value)
434
+ else if (Candy.Var(this.types[col]).isBegin('boolean')) value = parseInt(value)
435
+ else if (Candy.Var(this.types[col]).isBegin('json')) value = JSON.stringify(value)
436
+ else if (Candy.Var(this.types[col]).isBegin('date', 'datetime', 'timestamp')) value = Candy.Var(value).date('Y-m-d H:i:s')
437
+ }
438
+ return value
439
+ }
440
+
441
+ async #valuesExtract(arr) {
442
+ let query_key = []
443
+ let query_val = []
444
+ let multiple = false
445
+ let keys = Object.keys(arr)
446
+ for (let i = 0; i < keys.length; i++) {
447
+ let key = keys[i]
448
+ let val = arr[key]
449
+ // if(is_object($val)) $val = (array)$val;
450
+ // if(is_array($val) && !isset($this->table[$this->arr['table']]['columns'][$key]) && (!isset($val['ct']) || $val['ct']!=$GLOBALS['candy_token_mysql'])){
451
+ // $multiple = true;
452
+ // $ex = $this->valuesExtract($val);
453
+ // $query_key = $ex['into'];
454
+ // $query_val[] = $ex['values'];
455
+ /* }else */ if (val === null) {
456
+ query_key.push(this.escape(key, 'col'))
457
+ query_val.push('NULL')
458
+ } else {
459
+ query_key.push(this.escape(key, 'col'))
460
+ query_val.push(this.escape(await this.type(key, val, 'encode')))
461
+ }
462
+ }
463
+ return {
464
+ into: !multiple ? `(${query_key.join(',')})` : query_key,
465
+ values: !multiple ? `(${query_val.join(',')})` : query_val.join(',')
466
+ }
467
+ }
468
+
469
+ where(...args) {
470
+ if (args.length == 1 && typeof args[0] !== 'object' && !(args[0] instanceof Raw)) {
471
+ this.#arr.where = this.#whereExtract([this.#table[this.#arr.table].primary, args[0]])
472
+ } else if (args.length > 0) {
473
+ this.#arr.where =
474
+ this.#arr.where && this.#arr.where.trim() != '' ? `${this.#arr.where} AND ${this.#whereExtract(args)}` : this.#whereExtract(args)
475
+ }
476
+ return this
477
+ }
478
+
479
+ #whereExtract(arr) {
480
+ let q = ''
481
+ let loop = 1
482
+ let in_arr = false
483
+ let state = '='
484
+ let last = 0
485
+ for (const key of arr) {
486
+ if (key && Array.isArray(key) && state != 'IN' && state != 'NOT IN' && !(key instanceof Raw)) {
487
+ q += last == 1 ? ' AND ' + this.#whereExtract(key) : this.#whereExtract(key)
488
+ in_arr = true
489
+ last = 1
490
+ } else if (arr.length == 2 && loop == 2) {
491
+ q += ' = ' + this.escape(key)
492
+ } else if (in_arr) {
493
+ q += key.toUpperCase() == 'OR' ? ' OR ' : ' AND '
494
+ last = 2
495
+ } else if (arr.length == 3 && loop == 2) {
496
+ state = this.#statements.includes(key.toUpperCase()) ? key.toUpperCase() : '='
497
+ q += ' ' + state
498
+ last = 1
499
+ } else if (key === null) {
500
+ q += ' NULL '
501
+ } else {
502
+ q += this.escape(key, loop == 1 ? 'table' : 'value')
503
+ last = 1
504
+ }
505
+ loop++
506
+ }
507
+ return `(${q})`
508
+ }
509
+ }
510
+
511
+ module.exports = {
512
+ conn: {},
513
+ db: {},
514
+ init: function () {
515
+ return new Promise(resolve => {
516
+ if (!Candy.Config.database) return resolve(false)
517
+ let multiple = typeof Candy.Config.database[Object.keys(Candy.Config.database)[0]] === 'object'
518
+ let dbs = multiple ? Candy.Config.database : {default: Candy.Config.database}
519
+ for (let key of Object.keys(dbs)) {
520
+ let db = dbs[key]
521
+ if (db.type && db.type != 'mysql') continue
522
+ Candy.Mysql.conn[key] = mysql.createConnection({
523
+ host: db.host ?? '127.0.0.1',
524
+ user: db.user,
525
+ password: db.password,
526
+ database: db.database,
527
+ stringifyObjects: true
528
+ })
529
+ Candy.Mysql.conn[key].connect(err => {
530
+ if (err) {
531
+ console.error(`CandyPack Mysql Error: Failed to connect to database '${key}'`)
532
+ console.error(`Host: ${db.host ?? '127.0.0.1'}`)
533
+ console.error(`User: ${db.user}`)
534
+ console.error(`Database: ${db.database}`)
535
+ console.error(`Error: ${err.message}`)
536
+ return resolve(false)
537
+ }
538
+ })
539
+ Candy.Mysql.conn[key].query('SHOW TABLES', (err, result) => {
540
+ if (err) {
541
+ console.error(`CandyPack Mysql Error: Failed to query tables from database '${key}'`)
542
+ console.error(`Error: ${err.message}`)
543
+ return resolve(false)
544
+ }
545
+ for (let table of result)
546
+ for (let key of Object.keys(table)) {
547
+ let t = () => {
548
+ new Mysql(table[key], Candy.Mysql.conn['default'])
549
+ }
550
+ t()
551
+ }
552
+ })
553
+ }
554
+ return resolve(true)
555
+ })
556
+ },
557
+ database: function (name) {
558
+ if (!Candy.Mysql.conn[name]) return null
559
+ return new Mysql(name, Candy.Mysql.conn[name])
560
+ },
561
+ run: function (query, params) {
562
+ if (!Candy.Mysql.conn['default']) return Promise.resolve(false)
563
+ return new Mysql(null, Candy.Mysql.conn['default']).run(query, params)
564
+ },
565
+ table: function (name) {
566
+ if (!Candy.Mysql.conn['default']) return null
567
+ return new Mysql(name, Candy.Mysql.conn['default'])
568
+ },
569
+ raw: function (query) {
570
+ if (typeof query !== 'string') {
571
+ throw new Error('Mysql.raw() requires a string parameter')
572
+ }
573
+ return new Raw(query)
574
+ }
575
+ }