SqliteDatabaseAccessService.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. /*
  2. * Copyright (C) 2024 Puter Technologies Inc.
  3. *
  4. * This file is part of Puter.
  5. *
  6. * Puter is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License as published
  8. * by the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU Affero General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Affero General Public License
  17. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  18. */
  19. const { es_import_promise } = require("../../fun/dev-console-ui-utils");
  20. const { surrounding_box } = require("../../fun/dev-console-ui-utils");
  21. const { BaseDatabaseAccessService } = require("./BaseDatabaseAccessService");
  22. class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
  23. static ENGINE_NAME = 'sqlite';
  24. static MODULES = {
  25. // Documentation calls it 'Database'; it's new-able so
  26. // I'll stick with their convention over ours.
  27. Database: require('better-sqlite3'),
  28. };
  29. async _init () {
  30. const require = this.require;
  31. const Database = require('better-sqlite3');
  32. this._register_commands(this.services.get('commands'));
  33. const fs = require('fs');
  34. const path_ = require('path');
  35. const do_setup = ! fs.existsSync(this.config.path);
  36. this.db = new Database(this.config.path);
  37. // Database upgrade logic
  38. const TARGET_VERSION = 5;
  39. if ( do_setup ) {
  40. this.log.noticeme(`SETUP: creating database at ${this.config.path}`);
  41. const sql_files = [
  42. '0001_create-tables.sql',
  43. '0002_add-default-apps.sql',
  44. '0003_user-permissions.sql',
  45. '0004_sessions.sql',
  46. '0005_background-apps.sql',
  47. '0006_update-apps.sql',
  48. '0007_sessions.sql',
  49. ].map(p => path_.join(__dirname, 'sqlite_setup', p));
  50. const fs = require('fs');
  51. for ( const filename of sql_files ) {
  52. const basename = path_.basename(filename);
  53. this.log.noticeme(`applying ${basename}`);
  54. const contents = fs.readFileSync(filename, 'utf8');
  55. this.db.exec(contents);
  56. }
  57. await this.db.exec(`PRAGMA user_version = ${TARGET_VERSION};`);
  58. }
  59. const [{ user_version }] = await this._read('PRAGMA user_version');
  60. this.log.info('database version: ' + user_version);
  61. const upgrade_files = [];
  62. if ( user_version <= 0 ) {
  63. upgrade_files.push('0003_user-permissions.sql');
  64. }
  65. if ( user_version <= 1 ) {
  66. upgrade_files.push('0004_sessions.sql');
  67. }
  68. if ( user_version <= 2 ) {
  69. upgrade_files.push('0005_background-apps.sql');
  70. }
  71. if ( user_version <= 3 ) {
  72. upgrade_files.push('0006_update-apps.sql');
  73. }
  74. if ( user_version <= 4 ) {
  75. upgrade_files.push('0007_sessions.sql');
  76. }
  77. if ( upgrade_files.length > 0 ) {
  78. this.log.noticeme(`Database out of date: ${this.config.path}`);
  79. this.log.noticeme(`UPGRADING DATABASE: ${user_version} -> ${TARGET_VERSION}`);
  80. this.log.noticeme(`${upgrade_files.length} .sql files to apply`);
  81. const sql_files = upgrade_files.map(
  82. p => path_.join(__dirname, 'sqlite_setup', p)
  83. );
  84. const fs = require('fs');
  85. for ( const filename of sql_files ) {
  86. const basename = path_.basename(filename);
  87. this.log.noticeme(`applying ${basename}`);
  88. const contents = fs.readFileSync(filename, 'utf8');
  89. this.db.exec(contents);
  90. }
  91. // Update version number
  92. await this.db.exec(`PRAGMA user_version = ${TARGET_VERSION};`);
  93. // Add sticky notification
  94. this.database_update_notice = () => {
  95. const lines = [
  96. `Database has been updated!`,
  97. `Current version: ${TARGET_VERSION}`,
  98. `Type sqlite:dismiss to dismiss this message`,
  99. ];
  100. surrounding_box('33;1', lines);
  101. return lines;
  102. };
  103. (async () => {
  104. await es_import_promise;
  105. const svc_devConsole = this.services.get('dev-console');
  106. svc_devConsole.add_widget(this.database_update_notice);
  107. })();
  108. }
  109. }
  110. async _read (query, params = []) {
  111. query = this.sqlite_transform_query_(query);
  112. params = this.sqlite_transform_params_(params);
  113. return this.db.prepare(query).all(...params);
  114. }
  115. async _requireRead (query, params) {
  116. return this._read(query, params);
  117. }
  118. async _write (query, params) {
  119. query = this.sqlite_transform_query_(query);
  120. params = this.sqlite_transform_params_(params);
  121. try {
  122. const stmt = this.db.prepare(query);
  123. const info = stmt.run(...params);
  124. return {
  125. insertId: info.lastInsertRowid,
  126. anyRowsAffected: info.changes > 0,
  127. };
  128. } catch ( e ) {
  129. console.error(e);
  130. console.log('everything', {
  131. query, params,
  132. })
  133. console.log(params.map(p => typeof p));
  134. // throw e;
  135. }
  136. }
  137. async _batch_write (entries) {
  138. this.db.transaction(() => {
  139. for ( let { statement, values } of entries ) {
  140. statement = this.sqlite_transform_query_(statement);
  141. values = this.sqlite_transform_params_(values);
  142. this.db.prepare(statement).run(values);
  143. }
  144. })();
  145. }
  146. sqlite_transform_query_ (query) {
  147. // replace `now()` with `datetime('now')`
  148. query = query.replace(/now\(\)/g, 'datetime(\'now\')');
  149. return query;
  150. }
  151. sqlite_transform_params_ (params) {
  152. return params.map(p => {
  153. if ( typeof p === 'boolean' ) {
  154. return p ? 1 : 0;
  155. }
  156. return p;
  157. });
  158. }
  159. _register_commands (commands) {
  160. commands.registerCommands('sqlite', [
  161. {
  162. id: 'execfile',
  163. description: 'execute a file',
  164. handler: async (args, log) => {
  165. try {
  166. const [filename] = args;
  167. const fs = require('fs');
  168. const contents = fs.readFileSync(filename, 'utf8');
  169. this.db.exec(contents);
  170. } catch (err) {
  171. log.error(err.message);
  172. }
  173. }
  174. },
  175. {
  176. id: 'read',
  177. description: 'read a query',
  178. handler: async (args, log) => {
  179. try {
  180. const [query] = args;
  181. const rows = this._read(query, []);
  182. log.log(rows);
  183. } catch (err) {
  184. log.error(err.message);
  185. }
  186. }
  187. },
  188. {
  189. id: 'dismiss',
  190. description: 'dismiss the database update notice',
  191. handler: async (_, log) => {
  192. const svc_devConsole = this.services.get('dev-console');
  193. if ( ! svc_devConsole ) return;
  194. if ( ! this.database_update_notice ) return;
  195. svc_devConsole.remove_widget(this.database_update_notice);
  196. const lines = this.database_update_notice();
  197. for ( const line of lines ) log.log(line);
  198. this.database_update_notice = null;
  199. }
  200. }
  201. ])
  202. }
  203. }
  204. module.exports = {
  205. SqliteDatabaseAccessService,
  206. };