digitaltwin-core 0.14.2 → 1.0.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 (227) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +494 -359
  3. package/dist/auth/apisix_parser.d.ts +141 -0
  4. package/dist/auth/apisix_parser.d.ts.map +1 -0
  5. package/dist/auth/apisix_parser.js +161 -0
  6. package/dist/auth/apisix_parser.js.map +1 -0
  7. package/dist/auth/auth_config.d.ts +126 -0
  8. package/dist/auth/auth_config.d.ts.map +1 -0
  9. package/dist/auth/auth_config.js +169 -0
  10. package/dist/auth/auth_config.js.map +1 -0
  11. package/dist/auth/auth_provider.d.ts +118 -0
  12. package/dist/auth/auth_provider.d.ts.map +1 -0
  13. package/dist/auth/auth_provider.js +8 -0
  14. package/dist/auth/auth_provider.js.map +1 -0
  15. package/dist/auth/auth_provider_factory.d.ts +91 -0
  16. package/dist/auth/auth_provider_factory.d.ts.map +1 -0
  17. package/dist/auth/auth_provider_factory.js +146 -0
  18. package/dist/auth/auth_provider_factory.js.map +1 -0
  19. package/dist/auth/index.d.ts +8 -0
  20. package/dist/auth/index.d.ts.map +1 -0
  21. package/dist/auth/index.js +7 -0
  22. package/dist/auth/index.js.map +1 -0
  23. package/dist/auth/providers/gateway_auth_provider.d.ts +78 -0
  24. package/dist/auth/providers/gateway_auth_provider.d.ts.map +1 -0
  25. package/dist/auth/providers/gateway_auth_provider.js +109 -0
  26. package/dist/auth/providers/gateway_auth_provider.js.map +1 -0
  27. package/dist/auth/providers/index.d.ts +4 -0
  28. package/dist/auth/providers/index.d.ts.map +1 -0
  29. package/dist/auth/providers/index.js +4 -0
  30. package/dist/auth/providers/index.js.map +1 -0
  31. package/dist/auth/providers/jwt_auth_provider.d.ts +91 -0
  32. package/dist/auth/providers/jwt_auth_provider.d.ts.map +1 -0
  33. package/dist/auth/providers/jwt_auth_provider.js +204 -0
  34. package/dist/auth/providers/jwt_auth_provider.js.map +1 -0
  35. package/dist/auth/providers/no_auth_provider.d.ts +61 -0
  36. package/dist/auth/providers/no_auth_provider.d.ts.map +1 -0
  37. package/dist/auth/providers/no_auth_provider.js +76 -0
  38. package/dist/auth/providers/no_auth_provider.js.map +1 -0
  39. package/dist/auth/types.d.ts +100 -0
  40. package/dist/auth/types.d.ts.map +1 -0
  41. package/dist/auth/types.js +2 -0
  42. package/dist/auth/types.js.map +1 -0
  43. package/dist/auth/user_service.d.ts +86 -0
  44. package/dist/auth/user_service.d.ts.map +1 -0
  45. package/dist/auth/user_service.js +237 -0
  46. package/dist/auth/user_service.js.map +1 -0
  47. package/dist/components/assets_manager.d.ts +662 -0
  48. package/dist/components/assets_manager.d.ts.map +1 -0
  49. package/dist/components/assets_manager.js +1537 -0
  50. package/dist/components/assets_manager.js.map +1 -0
  51. package/dist/components/async_upload.d.ts +20 -0
  52. package/dist/components/async_upload.d.ts.map +1 -0
  53. package/dist/components/async_upload.js +10 -0
  54. package/dist/components/async_upload.js.map +1 -0
  55. package/dist/components/collector.d.ts +203 -0
  56. package/dist/components/collector.d.ts.map +1 -0
  57. package/dist/components/collector.js +214 -0
  58. package/dist/components/collector.js.map +1 -0
  59. package/dist/components/custom_table_manager.d.ts +503 -0
  60. package/dist/components/custom_table_manager.d.ts.map +1 -0
  61. package/dist/components/custom_table_manager.js +1023 -0
  62. package/dist/components/custom_table_manager.js.map +1 -0
  63. package/dist/components/global_assets_handler.d.ts +63 -0
  64. package/dist/components/global_assets_handler.d.ts.map +1 -0
  65. package/dist/components/global_assets_handler.js +127 -0
  66. package/dist/components/global_assets_handler.js.map +1 -0
  67. package/dist/components/handler.d.ts +104 -0
  68. package/dist/components/handler.d.ts.map +1 -0
  69. package/dist/components/handler.js +110 -0
  70. package/dist/components/handler.js.map +1 -0
  71. package/dist/components/harvester.d.ts +182 -0
  72. package/dist/components/harvester.d.ts.map +1 -0
  73. package/dist/components/harvester.js +406 -0
  74. package/dist/components/harvester.js.map +1 -0
  75. package/dist/components/index.d.ts +11 -0
  76. package/dist/components/index.d.ts.map +1 -0
  77. package/dist/components/index.js +9 -0
  78. package/dist/components/index.js.map +1 -0
  79. package/dist/components/interfaces.d.ts +126 -0
  80. package/dist/components/interfaces.d.ts.map +1 -0
  81. package/dist/components/interfaces.js +8 -0
  82. package/dist/components/interfaces.js.map +1 -0
  83. package/dist/components/map_manager.d.ts +61 -0
  84. package/dist/components/map_manager.d.ts.map +1 -0
  85. package/dist/components/map_manager.js +242 -0
  86. package/dist/components/map_manager.js.map +1 -0
  87. package/dist/components/tileset_manager.d.ts +125 -0
  88. package/dist/components/tileset_manager.d.ts.map +1 -0
  89. package/dist/components/tileset_manager.js +623 -0
  90. package/dist/components/tileset_manager.js.map +1 -0
  91. package/dist/components/types.d.ts +226 -0
  92. package/dist/components/types.d.ts.map +1 -0
  93. package/dist/components/types.js +8 -0
  94. package/dist/components/types.js.map +1 -0
  95. package/dist/database/adapters/knex_database_adapter.d.ts +97 -0
  96. package/dist/database/adapters/knex_database_adapter.d.ts.map +1 -0
  97. package/dist/database/adapters/knex_database_adapter.js +729 -0
  98. package/dist/database/adapters/knex_database_adapter.js.map +1 -0
  99. package/dist/database/database_adapter.d.ts +262 -0
  100. package/dist/database/database_adapter.d.ts.map +1 -0
  101. package/dist/database/database_adapter.js +46 -0
  102. package/dist/database/database_adapter.js.map +1 -0
  103. package/dist/engine/digital_twin_engine.d.ts +295 -0
  104. package/dist/engine/digital_twin_engine.d.ts.map +1 -0
  105. package/dist/engine/digital_twin_engine.js +907 -0
  106. package/dist/engine/digital_twin_engine.js.map +1 -0
  107. package/dist/engine/endpoints.d.ts +47 -0
  108. package/dist/engine/endpoints.d.ts.map +1 -0
  109. package/dist/engine/endpoints.js +88 -0
  110. package/dist/engine/endpoints.js.map +1 -0
  111. package/dist/engine/error_handler.d.ts +20 -0
  112. package/dist/engine/error_handler.d.ts.map +1 -0
  113. package/dist/engine/error_handler.js +69 -0
  114. package/dist/engine/error_handler.js.map +1 -0
  115. package/dist/engine/events.d.ts +93 -0
  116. package/dist/engine/events.d.ts.map +1 -0
  117. package/dist/engine/events.js +71 -0
  118. package/dist/engine/events.js.map +1 -0
  119. package/dist/engine/health.d.ts +112 -0
  120. package/dist/engine/health.d.ts.map +1 -0
  121. package/dist/engine/health.js +190 -0
  122. package/dist/engine/health.js.map +1 -0
  123. package/dist/engine/initializer.d.ts +62 -0
  124. package/dist/engine/initializer.d.ts.map +1 -0
  125. package/dist/engine/initializer.js +108 -0
  126. package/dist/engine/initializer.js.map +1 -0
  127. package/dist/engine/queue_manager.d.ts +87 -0
  128. package/dist/engine/queue_manager.d.ts.map +1 -0
  129. package/dist/engine/queue_manager.js +196 -0
  130. package/dist/engine/queue_manager.js.map +1 -0
  131. package/dist/engine/scheduler.d.ts +30 -0
  132. package/dist/engine/scheduler.d.ts.map +1 -0
  133. package/dist/engine/scheduler.js +378 -0
  134. package/dist/engine/scheduler.js.map +1 -0
  135. package/dist/engine/upload_processor.d.ts +36 -0
  136. package/dist/engine/upload_processor.d.ts.map +1 -0
  137. package/dist/engine/upload_processor.js +113 -0
  138. package/dist/engine/upload_processor.js.map +1 -0
  139. package/dist/env/env.d.ts +134 -0
  140. package/dist/env/env.d.ts.map +1 -0
  141. package/dist/env/env.js +177 -0
  142. package/dist/env/env.js.map +1 -0
  143. package/dist/errors/index.d.ts +94 -0
  144. package/dist/errors/index.d.ts.map +1 -0
  145. package/dist/errors/index.js +149 -0
  146. package/dist/errors/index.js.map +1 -0
  147. package/dist/index.d.ts +55 -0
  148. package/dist/index.d.ts.map +1 -0
  149. package/dist/index.js +65 -0
  150. package/dist/index.js.map +1 -0
  151. package/dist/openapi/generator.d.ts +93 -0
  152. package/dist/openapi/generator.d.ts.map +1 -0
  153. package/dist/openapi/generator.js +293 -0
  154. package/dist/openapi/generator.js.map +1 -0
  155. package/dist/openapi/index.d.ts +9 -0
  156. package/dist/openapi/index.d.ts.map +1 -0
  157. package/dist/openapi/index.js +9 -0
  158. package/dist/openapi/index.js.map +1 -0
  159. package/dist/openapi/types.d.ts +182 -0
  160. package/dist/openapi/types.d.ts.map +1 -0
  161. package/dist/openapi/types.js +16 -0
  162. package/dist/openapi/types.js.map +1 -0
  163. package/dist/storage/adapters/local_storage_service.d.ts +57 -0
  164. package/dist/storage/adapters/local_storage_service.d.ts.map +1 -0
  165. package/dist/storage/adapters/local_storage_service.js +132 -0
  166. package/dist/storage/adapters/local_storage_service.js.map +1 -0
  167. package/dist/storage/adapters/ovh_storage_service.d.ts +72 -0
  168. package/dist/storage/adapters/ovh_storage_service.d.ts.map +1 -0
  169. package/dist/storage/adapters/ovh_storage_service.js +205 -0
  170. package/dist/storage/adapters/ovh_storage_service.js.map +1 -0
  171. package/dist/storage/storage_factory.d.ts +14 -0
  172. package/dist/storage/storage_factory.d.ts.map +1 -0
  173. package/dist/storage/storage_factory.js +43 -0
  174. package/dist/storage/storage_factory.js.map +1 -0
  175. package/dist/storage/storage_service.d.ts +163 -0
  176. package/dist/storage/storage_service.d.ts.map +1 -0
  177. package/dist/storage/storage_service.js +58 -0
  178. package/dist/storage/storage_service.js.map +1 -0
  179. package/dist/types/data_record.d.ts +123 -0
  180. package/dist/types/data_record.d.ts.map +1 -0
  181. package/dist/types/data_record.js +8 -0
  182. package/dist/types/data_record.js.map +1 -0
  183. package/dist/utils/graceful_shutdown.d.ts +44 -0
  184. package/dist/utils/graceful_shutdown.d.ts.map +1 -0
  185. package/dist/utils/graceful_shutdown.js +79 -0
  186. package/dist/utils/graceful_shutdown.js.map +1 -0
  187. package/dist/utils/http_responses.d.ts +175 -0
  188. package/dist/utils/http_responses.d.ts.map +1 -0
  189. package/dist/utils/http_responses.js +216 -0
  190. package/dist/utils/http_responses.js.map +1 -0
  191. package/dist/utils/index.d.ts +8 -0
  192. package/dist/utils/index.d.ts.map +1 -0
  193. package/dist/utils/index.js +6 -0
  194. package/dist/utils/index.js.map +1 -0
  195. package/dist/utils/logger.d.ts +74 -0
  196. package/dist/utils/logger.d.ts.map +1 -0
  197. package/dist/utils/logger.js +92 -0
  198. package/dist/utils/logger.js.map +1 -0
  199. package/dist/utils/map_to_data_record.d.ts +10 -0
  200. package/dist/utils/map_to_data_record.d.ts.map +1 -0
  201. package/dist/utils/map_to_data_record.js +36 -0
  202. package/dist/utils/map_to_data_record.js.map +1 -0
  203. package/dist/utils/safe_async.d.ts +50 -0
  204. package/dist/utils/safe_async.d.ts.map +1 -0
  205. package/dist/utils/safe_async.js +90 -0
  206. package/dist/utils/safe_async.js.map +1 -0
  207. package/dist/utils/servable_endpoint.d.ts +63 -0
  208. package/dist/utils/servable_endpoint.d.ts.map +1 -0
  209. package/dist/utils/servable_endpoint.js +67 -0
  210. package/dist/utils/servable_endpoint.js.map +1 -0
  211. package/dist/utils/zip_utils.d.ts +66 -0
  212. package/dist/utils/zip_utils.d.ts.map +1 -0
  213. package/dist/utils/zip_utils.js +169 -0
  214. package/dist/utils/zip_utils.js.map +1 -0
  215. package/dist/validation/index.d.ts +3 -0
  216. package/dist/validation/index.d.ts.map +1 -0
  217. package/dist/validation/index.js +7 -0
  218. package/dist/validation/index.js.map +1 -0
  219. package/dist/validation/schemas.d.ts +273 -0
  220. package/dist/validation/schemas.d.ts.map +1 -0
  221. package/dist/validation/schemas.js +82 -0
  222. package/dist/validation/schemas.js.map +1 -0
  223. package/dist/validation/validate.d.ts +49 -0
  224. package/dist/validation/validate.d.ts.map +1 -0
  225. package/dist/validation/validate.js +110 -0
  226. package/dist/validation/validate.js.map +1 -0
  227. package/package.json +23 -13
@@ -0,0 +1,729 @@
1
+ import knex from 'knex';
2
+ import { DatabaseAdapter } from '../database_adapter.js';
3
+ import { mapToDataRecord } from '../../utils/map_to_data_record.js';
4
+ /**
5
+ * Knex-based implementation with extended querying capabilities.
6
+ */
7
+ export class KnexDatabaseAdapter extends DatabaseAdapter {
8
+ #knex;
9
+ #storage;
10
+ constructor(config, storage) {
11
+ super();
12
+ this.#knex = knex(config);
13
+ this.#storage = storage;
14
+ }
15
+ /**
16
+ * Create a KnexDatabaseAdapter for PostgreSQL with simplified configuration
17
+ */
18
+ static forPostgreSQL(pgConfig, storage, _tableName = 'data_index') {
19
+ const knexConfig = {
20
+ client: 'pg',
21
+ connection: {
22
+ host: pgConfig.host,
23
+ port: pgConfig.port || 5432,
24
+ user: pgConfig.user,
25
+ password: pgConfig.password,
26
+ database: pgConfig.database,
27
+ ssl: pgConfig.ssl || false
28
+ },
29
+ pool: {
30
+ min: 2,
31
+ max: 15,
32
+ acquireTimeoutMillis: 30000,
33
+ createTimeoutMillis: 30000,
34
+ destroyTimeoutMillis: 5000,
35
+ idleTimeoutMillis: 30000,
36
+ reapIntervalMillis: 1000
37
+ }
38
+ };
39
+ return new KnexDatabaseAdapter(knexConfig, storage);
40
+ }
41
+ /**
42
+ * Create a KnexDatabaseAdapter for SQLite with simplified configuration
43
+ */
44
+ static forSQLite(sqliteConfig, storage, _tableName = 'data_index') {
45
+ const client = sqliteConfig.client || 'sqlite3';
46
+ const knexConfig = {
47
+ client,
48
+ connection: {
49
+ filename: sqliteConfig.filename
50
+ },
51
+ pool: {
52
+ min: 1,
53
+ max: 5,
54
+ acquireTimeoutMillis: sqliteConfig.busyTimeout || 30000,
55
+ afterCreate: (conn, cb) => {
56
+ if (sqliteConfig.enableForeignKeys !== false) {
57
+ // Both sqlite3 and better-sqlite3 support PRAGMA
58
+ if (client === 'better-sqlite3') {
59
+ conn.pragma('foreign_keys = ON');
60
+ conn.pragma('journal_mode = WAL');
61
+ conn.pragma('synchronous = NORMAL');
62
+ conn.pragma('cache_size = 10000');
63
+ cb();
64
+ }
65
+ else {
66
+ conn.run('PRAGMA foreign_keys = ON', () => {
67
+ conn.run('PRAGMA journal_mode = WAL', () => {
68
+ conn.run('PRAGMA synchronous = NORMAL', () => {
69
+ conn.run('PRAGMA cache_size = 10000', cb);
70
+ });
71
+ });
72
+ });
73
+ }
74
+ }
75
+ else {
76
+ cb();
77
+ }
78
+ }
79
+ },
80
+ useNullAsDefault: true
81
+ };
82
+ return new KnexDatabaseAdapter(knexConfig, storage);
83
+ }
84
+ // ========== Basic methods ==========
85
+ /**
86
+ * Validates that a table name is safe for SQL operations.
87
+ * Prevents SQL injection via table names.
88
+ * @param name - The table name to validate
89
+ * @throws Error if the table name is invalid
90
+ */
91
+ #validateTableName(name) {
92
+ // Must start with letter or underscore, followed by alphanumeric or underscores
93
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
94
+ throw new Error(`Invalid table name: "${name}". Must start with a letter or underscore and contain only alphanumeric characters and underscores.`);
95
+ }
96
+ // PostgreSQL max identifier length is 63, SQLite has no practical limit
97
+ if (name.length > 63) {
98
+ throw new Error(`Table name too long: "${name}". Maximum 63 characters allowed.`);
99
+ }
100
+ }
101
+ async save(meta) {
102
+ this.#validateTableName(meta.name);
103
+ const insertData = {
104
+ id: meta.id,
105
+ name: meta.name,
106
+ type: meta.type,
107
+ url: meta.url,
108
+ date: meta.date.toISOString()
109
+ };
110
+ // Add asset-specific fields if present (for AssetMetadataRow)
111
+ if ('description' in meta)
112
+ insertData.description = meta.description;
113
+ if ('source' in meta)
114
+ insertData.source = meta.source;
115
+ if ('owner_id' in meta)
116
+ insertData.owner_id = meta.owner_id;
117
+ if ('filename' in meta)
118
+ insertData.filename = meta.filename;
119
+ if ('is_public' in meta)
120
+ insertData.is_public = meta.is_public;
121
+ // TilesetManager support (public URL)
122
+ if ('tileset_url' in meta)
123
+ insertData.tileset_url = meta.tileset_url;
124
+ // Async upload support
125
+ if ('upload_status' in meta)
126
+ insertData.upload_status = meta.upload_status;
127
+ if ('upload_error' in meta)
128
+ insertData.upload_error = meta.upload_error;
129
+ if ('upload_job_id' in meta)
130
+ insertData.upload_job_id = meta.upload_job_id;
131
+ // Insert and get the auto-generated ID
132
+ const [insertedId] = await this.#knex(meta.name).insert(insertData).returning('id');
133
+ // Handle different return formats (PostgreSQL returns object, SQLite returns number)
134
+ const newId = typeof insertedId === 'object' ? insertedId.id : insertedId;
135
+ // Return record with the generated ID
136
+ return mapToDataRecord({ ...meta, id: newId }, this.#storage);
137
+ }
138
+ async delete(id, name) {
139
+ this.#validateTableName(name);
140
+ await this.#knex(name).where({ id }).delete();
141
+ }
142
+ async getById(id, name) {
143
+ this.#validateTableName(name);
144
+ const row = await this.#knex(name).where({ id }).first();
145
+ return row ? mapToDataRecord(row, this.#storage) : undefined;
146
+ }
147
+ async getLatestByName(name) {
148
+ this.#validateTableName(name);
149
+ const row = await this.#knex(name).select('*').orderBy('date', 'desc').limit(1).first();
150
+ return row ? mapToDataRecord(row, this.#storage) : undefined;
151
+ }
152
+ async doesTableExists(name) {
153
+ this.#validateTableName(name);
154
+ return this.#knex.schema.hasTable(name);
155
+ }
156
+ async createTable(name) {
157
+ this.#validateTableName(name);
158
+ const tableExists = await this.#knex.schema.hasTable(name);
159
+ if (!tableExists) {
160
+ await this.#knex.schema.createTable(name, table => {
161
+ table.increments('id').primary();
162
+ table.string('name').notNullable();
163
+ table.string('type').notNullable();
164
+ table.string('url').notNullable();
165
+ table.datetime('date').notNullable();
166
+ // Asset-specific fields (optional, for AssetsManager components)
167
+ table.text('description').nullable();
168
+ table.string('source').nullable();
169
+ table.integer('owner_id').unsigned().nullable();
170
+ table.string('filename').nullable();
171
+ table.boolean('is_public').defaultTo(true).notNullable();
172
+ // TilesetManager support (public URL for Cesium)
173
+ table.text('tileset_url').nullable();
174
+ // Async upload support (for large file processing)
175
+ table.string('upload_status', 20).nullable(); // pending, processing, completed, failed
176
+ table.text('upload_error').nullable();
177
+ table.string('upload_job_id', 100).nullable(); // BullMQ job ID for status tracking
178
+ // Foreign key constraint to users table (if it exists)
179
+ // Note: This will only work if users table exists first
180
+ try {
181
+ table.foreign('owner_id').references('id').inTable('users').onDelete('SET NULL');
182
+ }
183
+ catch {
184
+ // Ignore foreign key creation if users table doesn't exist yet
185
+ // This allows backward compatibility for non-authenticated assets
186
+ }
187
+ // Optimized indexes for most frequent queries
188
+ table.index('name', `${name}_idx_name`);
189
+ table.index('date', `${name}_idx_date`);
190
+ table.index(['name', 'date'], `${name}_idx_name_date`);
191
+ table.index(['date', 'name'], `${name}_idx_date_name`); // For date range queries
192
+ table.index('owner_id', `${name}_idx_owner_id`); // For asset filtering and foreign key
193
+ table.index('is_public', `${name}_idx_is_public`); // For visibility filtering
194
+ });
195
+ }
196
+ }
197
+ async createTableWithColumns(name, columns) {
198
+ this.#validateTableName(name);
199
+ const tableExists = await this.#knex.schema.hasTable(name);
200
+ if (!tableExists) {
201
+ await this.#knex.schema.createTable(name, table => {
202
+ // Standard columns for CustomTableManager
203
+ table.increments('id').primary();
204
+ table.datetime('created_at').defaultTo(this.#knex.fn.now()).notNullable();
205
+ table.datetime('updated_at').defaultTo(this.#knex.fn.now()).notNullable();
206
+ // Custom columns from StoreConfiguration
207
+ for (const [columnName, columnType] of Object.entries(columns)) {
208
+ // Parse SQL type and apply it to the table
209
+ this.#addColumnToTable(table, columnName, columnType);
210
+ }
211
+ // Indexes for performance
212
+ table.index('created_at', `${name}_idx_created_at`);
213
+ table.index('updated_at', `${name}_idx_updated_at`);
214
+ });
215
+ }
216
+ }
217
+ /**
218
+ * Helper method to add a column to a Knex table based on SQL type string
219
+ * @private
220
+ */
221
+ #addColumnToTable(table, columnName, sqlType) {
222
+ const lowerType = sqlType.toLowerCase();
223
+ if (lowerType.includes('text')) {
224
+ const col = table.text(columnName);
225
+ if (lowerType.includes('not null'))
226
+ col.notNullable();
227
+ else
228
+ col.nullable();
229
+ }
230
+ else if (lowerType.includes('integer')) {
231
+ const col = table.integer(columnName);
232
+ if (lowerType.includes('not null'))
233
+ col.notNullable();
234
+ else
235
+ col.nullable();
236
+ }
237
+ else if (lowerType.includes('boolean')) {
238
+ const col = table.boolean(columnName);
239
+ if (lowerType.includes('not null'))
240
+ col.notNullable();
241
+ else
242
+ col.nullable();
243
+ if (lowerType.includes('default true'))
244
+ col.defaultTo(true);
245
+ else if (lowerType.includes('default false'))
246
+ col.defaultTo(false);
247
+ }
248
+ else if (lowerType.includes('timestamp') || lowerType.includes('datetime')) {
249
+ const col = table.datetime(columnName);
250
+ if (lowerType.includes('not null'))
251
+ col.notNullable();
252
+ else
253
+ col.nullable();
254
+ if (lowerType.includes('default current_timestamp'))
255
+ col.defaultTo(this.#knex.fn.now());
256
+ }
257
+ else if (lowerType.includes('real') || lowerType.includes('decimal') || lowerType.includes('float')) {
258
+ const col = table.decimal(columnName);
259
+ if (lowerType.includes('not null'))
260
+ col.notNullable();
261
+ else
262
+ col.nullable();
263
+ }
264
+ else if (lowerType.includes('varchar')) {
265
+ // Extract length from varchar(255)
266
+ const match = lowerType.match(/varchar\((\d+)\)/);
267
+ const length = match ? parseInt(match[1]) : 255;
268
+ const col = table.string(columnName, length);
269
+ if (lowerType.includes('not null'))
270
+ col.notNullable();
271
+ else
272
+ col.nullable();
273
+ }
274
+ else {
275
+ // Default to string for unknown types
276
+ const col = table.string(columnName);
277
+ if (lowerType.includes('not null'))
278
+ col.notNullable();
279
+ else
280
+ col.nullable();
281
+ }
282
+ }
283
+ /**
284
+ * Migrate existing table schema to match expected schema.
285
+ *
286
+ * Automatically adds missing columns and indexes for asset tables.
287
+ * Only performs safe operations (adding columns with defaults or nullable).
288
+ *
289
+ * @param {string} name - Table name to migrate
290
+ * @returns {Promise<string[]>} Array of migration messages describing what was done
291
+ */
292
+ async migrateTableSchema(name) {
293
+ this.#validateTableName(name);
294
+ const tableExists = await this.#knex.schema.hasTable(name);
295
+ if (!tableExists) {
296
+ return []; // Table doesn't exist, nothing to migrate
297
+ }
298
+ const migrations = [];
299
+ // Define expected columns for asset tables (those created by createTable)
300
+ const expectedColumns = {
301
+ is_public: {
302
+ exists: await this.#knex.schema.hasColumn(name, 'is_public'),
303
+ add: async () => {
304
+ await this.#knex.schema.alterTable(name, table => {
305
+ table.boolean('is_public').defaultTo(true).notNullable();
306
+ });
307
+ migrations.push(`Added column 'is_public' (BOOLEAN DEFAULT true NOT NULL)`);
308
+ }
309
+ },
310
+ tileset_url: {
311
+ exists: await this.#knex.schema.hasColumn(name, 'tileset_url'),
312
+ add: async () => {
313
+ await this.#knex.schema.alterTable(name, table => {
314
+ table.text('tileset_url').nullable();
315
+ });
316
+ migrations.push(`Added column 'tileset_url' (TEXT nullable)`);
317
+ }
318
+ },
319
+ upload_status: {
320
+ exists: await this.#knex.schema.hasColumn(name, 'upload_status'),
321
+ add: async () => {
322
+ await this.#knex.schema.alterTable(name, table => {
323
+ table.string('upload_status', 20).nullable().defaultTo(null);
324
+ });
325
+ migrations.push(`Added column 'upload_status' (VARCHAR(20) nullable)`);
326
+ }
327
+ },
328
+ upload_error: {
329
+ exists: await this.#knex.schema.hasColumn(name, 'upload_error'),
330
+ add: async () => {
331
+ await this.#knex.schema.alterTable(name, table => {
332
+ table.text('upload_error').nullable();
333
+ });
334
+ migrations.push(`Added column 'upload_error' (TEXT nullable)`);
335
+ }
336
+ },
337
+ upload_job_id: {
338
+ exists: await this.#knex.schema.hasColumn(name, 'upload_job_id'),
339
+ add: async () => {
340
+ await this.#knex.schema.alterTable(name, table => {
341
+ table.string('upload_job_id', 100).nullable();
342
+ });
343
+ migrations.push(`Added column 'upload_job_id' (VARCHAR(100) nullable)`);
344
+ }
345
+ },
346
+ created_at: {
347
+ exists: await this.#knex.schema.hasColumn(name, 'created_at'),
348
+ add: async () => {
349
+ await this.#knex.schema.alterTable(name, table => {
350
+ table.datetime('created_at').defaultTo(this.#knex.fn.now()).nullable();
351
+ });
352
+ migrations.push(`Added column 'created_at' (DATETIME nullable)`);
353
+ }
354
+ },
355
+ updated_at: {
356
+ exists: await this.#knex.schema.hasColumn(name, 'updated_at'),
357
+ add: async () => {
358
+ await this.#knex.schema.alterTable(name, table => {
359
+ table.datetime('updated_at').defaultTo(this.#knex.fn.now()).nullable();
360
+ });
361
+ migrations.push(`Added column 'updated_at' (DATETIME nullable)`);
362
+ }
363
+ }
364
+ };
365
+ // Expected indexes
366
+ const expectedIndexes = {
367
+ [`${name}_idx_is_public`]: {
368
+ exists: await this.#hasIndex(name, `${name}_idx_is_public`),
369
+ add: async () => {
370
+ await this.#knex.schema.alterTable(name, table => {
371
+ table.index('is_public', `${name}_idx_is_public`);
372
+ });
373
+ migrations.push(`Added index '${name}_idx_is_public'`);
374
+ }
375
+ }
376
+ };
377
+ // Add missing columns
378
+ for (const [_columnName, config] of Object.entries(expectedColumns)) {
379
+ if (!config.exists) {
380
+ await config.add();
381
+ }
382
+ }
383
+ // Add missing indexes
384
+ for (const [_indexName, config] of Object.entries(expectedIndexes)) {
385
+ if (!config.exists) {
386
+ await config.add();
387
+ }
388
+ }
389
+ return migrations;
390
+ }
391
+ /**
392
+ * Check if an index exists on a table
393
+ * @private
394
+ */
395
+ async #hasIndex(tableName, indexName) {
396
+ try {
397
+ // PostgreSQL
398
+ if (this.#knex.client.config.client === 'pg') {
399
+ const result = await this.#knex.raw(`SELECT 1 FROM pg_indexes WHERE tablename = ? AND indexname = ?`, [
400
+ tableName,
401
+ indexName
402
+ ]);
403
+ return result.rows.length > 0;
404
+ }
405
+ // SQLite - query sqlite_master
406
+ if (this.#knex.client.config.client === 'sqlite3' || this.#knex.client.config.client === 'better-sqlite3') {
407
+ const result = await this.#knex.raw(`SELECT name FROM sqlite_master WHERE type='index' AND name=?`, [
408
+ indexName
409
+ ]);
410
+ return result.length > 0;
411
+ }
412
+ // Unknown database, assume index doesn't exist
413
+ return false;
414
+ }
415
+ catch {
416
+ // If query fails, assume index doesn't exist
417
+ return false;
418
+ }
419
+ }
420
+ // ========== Extended methods ==========
421
+ async getFirstByName(name) {
422
+ this.#validateTableName(name);
423
+ const row = await this.#knex(name).orderBy('date', 'asc').first();
424
+ return row ? mapToDataRecord(row, this.#storage) : undefined;
425
+ }
426
+ async getByDateRange(name, startDate, endDate, limit) {
427
+ this.#validateTableName(name);
428
+ let query = this.#knex(name).select('*').where('date', '>=', startDate.toISOString());
429
+ if (endDate) {
430
+ query = query.where('date', '<', endDate.toISOString());
431
+ }
432
+ query = query.orderBy('date', 'asc');
433
+ if (limit) {
434
+ query = query.limit(limit);
435
+ }
436
+ const rows = await query;
437
+ return rows.map(row => mapToDataRecord(row, this.#storage));
438
+ }
439
+ async getAfterDate(name, afterDate, limit) {
440
+ this.#validateTableName(name);
441
+ let query = this.#knex(name).where('date', '>', afterDate.toISOString()).orderBy('date', 'asc');
442
+ if (limit) {
443
+ query = query.limit(limit);
444
+ }
445
+ const rows = await query;
446
+ return rows.map(row => mapToDataRecord(row, this.#storage));
447
+ }
448
+ async getLatestBefore(name, beforeDate) {
449
+ this.#validateTableName(name);
450
+ const row = await this.#knex(name).where('date', '<', beforeDate.toISOString()).orderBy('date', 'desc').first();
451
+ return row ? mapToDataRecord(row, this.#storage) : undefined;
452
+ }
453
+ async getLatestRecordsBefore(name, beforeDate, limit) {
454
+ this.#validateTableName(name);
455
+ const rows = await this.#knex(name)
456
+ .where('date', '<', beforeDate.toISOString())
457
+ .orderBy('date', 'desc')
458
+ .limit(limit);
459
+ return rows.map(row => mapToDataRecord(row, this.#storage));
460
+ }
461
+ async hasRecordsAfterDate(name, afterDate) {
462
+ this.#validateTableName(name);
463
+ const result = await this.#knex(name)
464
+ .where('date', '>', afterDate.toISOString())
465
+ .select(this.#knex.raw('1'))
466
+ .limit(1)
467
+ .first();
468
+ return !!result;
469
+ }
470
+ async countByDateRange(name, startDate, endDate) {
471
+ this.#validateTableName(name);
472
+ let query = this.#knex(name).where('date', '>=', startDate.toISOString());
473
+ if (endDate) {
474
+ query = query.where('date', '<', endDate.toISOString());
475
+ }
476
+ const result = await query.count('* as count').first();
477
+ return Number(result?.count) || 0;
478
+ }
479
+ // ========== Batch operations for performance ==========
480
+ async saveBatch(metadataList) {
481
+ if (metadataList.length === 0)
482
+ return [];
483
+ // Validate all table names upfront
484
+ for (const meta of metadataList) {
485
+ this.#validateTableName(meta.name);
486
+ }
487
+ // Group by table name for efficient batch inserts
488
+ const groupedByTable = new Map();
489
+ for (const meta of metadataList) {
490
+ const group = groupedByTable.get(meta.name);
491
+ if (group) {
492
+ group.push(meta);
493
+ }
494
+ else {
495
+ groupedByTable.set(meta.name, [meta]);
496
+ }
497
+ }
498
+ // Use transaction for atomicity - all or nothing
499
+ return this.#knex.transaction(async (trx) => {
500
+ const results = [];
501
+ for (const [tableName, metas] of groupedByTable) {
502
+ const insertData = metas.map(meta => {
503
+ const data = {
504
+ name: meta.name,
505
+ type: meta.type,
506
+ url: meta.url,
507
+ date: meta.date.toISOString()
508
+ };
509
+ // Only include ID if it's explicitly set (for updates)
510
+ if (meta.id !== undefined) {
511
+ data.id = meta.id;
512
+ }
513
+ // Add asset-specific fields if present
514
+ if ('description' in meta)
515
+ data.description = meta.description;
516
+ if ('source' in meta)
517
+ data.source = meta.source;
518
+ if ('owner_id' in meta)
519
+ data.owner_id = meta.owner_id;
520
+ if ('filename' in meta)
521
+ data.filename = meta.filename;
522
+ return data;
523
+ });
524
+ await trx(tableName).insert(insertData);
525
+ // Convert to DataRecords
526
+ for (const meta of metas) {
527
+ results.push(mapToDataRecord(meta, this.#storage));
528
+ }
529
+ }
530
+ return results;
531
+ });
532
+ }
533
+ async deleteBatch(deleteRequests) {
534
+ if (deleteRequests.length === 0)
535
+ return;
536
+ // Validate all table names upfront
537
+ for (const req of deleteRequests) {
538
+ this.#validateTableName(req.name);
539
+ }
540
+ // Group by table name for efficient batch deletes
541
+ const groupedByTable = new Map();
542
+ for (const req of deleteRequests) {
543
+ const group = groupedByTable.get(req.name);
544
+ if (group) {
545
+ group.push(req.id);
546
+ }
547
+ else {
548
+ groupedByTable.set(req.name, [req.id]);
549
+ }
550
+ }
551
+ // Use transaction for atomicity - all or nothing
552
+ await this.#knex.transaction(async (trx) => {
553
+ for (const [tableName, ids] of groupedByTable) {
554
+ await trx(tableName).whereIn('id', ids).delete();
555
+ }
556
+ });
557
+ }
558
+ async getByIdsBatch(requests) {
559
+ if (requests.length === 0)
560
+ return [];
561
+ // Validate all table names upfront
562
+ for (const req of requests) {
563
+ this.#validateTableName(req.name);
564
+ }
565
+ const results = [];
566
+ // Group by table name for efficient queries
567
+ const groupedByTable = new Map();
568
+ for (const req of requests) {
569
+ const group = groupedByTable.get(req.name);
570
+ if (group) {
571
+ group.push(req.id);
572
+ }
573
+ else {
574
+ groupedByTable.set(req.name, [req.id]);
575
+ }
576
+ }
577
+ // Query each table
578
+ for (const [tableName, ids] of groupedByTable) {
579
+ const rows = await this.#knex(tableName).whereIn('id', ids);
580
+ for (const row of rows) {
581
+ results.push(mapToDataRecord(row, this.#storage));
582
+ }
583
+ }
584
+ return results;
585
+ }
586
+ // ========== Optimized query for assets manager ==========
587
+ async getAllAssetsPaginated(name, offset = 0, limit = 100) {
588
+ this.#validateTableName(name);
589
+ // Get total count efficiently
590
+ const countResult = await this.#knex(name).count('* as count').first();
591
+ const total = Number(countResult?.count) || 0;
592
+ // Get paginated results
593
+ const rows = await this.#knex(name).select('*').orderBy('date', 'desc').offset(offset).limit(limit);
594
+ const records = rows.map(row => mapToDataRecord(row, this.#storage));
595
+ return { records, total };
596
+ }
597
+ async updateAssetMetadata(tableName, id, data) {
598
+ this.#validateTableName(tableName);
599
+ const updateData = {};
600
+ // Only update fields that are explicitly provided
601
+ if (data.description !== undefined)
602
+ updateData.description = data.description;
603
+ if (data.source !== undefined)
604
+ updateData.source = data.source;
605
+ if (data.is_public !== undefined)
606
+ updateData.is_public = data.is_public;
607
+ if (Object.keys(updateData).length === 0) {
608
+ // Nothing to update, just return the existing record
609
+ const existing = await this.getById(String(id), tableName);
610
+ if (!existing) {
611
+ throw new Error(`Record with ID ${id} not found in table ${tableName}`);
612
+ }
613
+ return existing;
614
+ }
615
+ const rowsAffected = await this.#knex(tableName).where('id', id).update(updateData);
616
+ if (rowsAffected === 0) {
617
+ throw new Error(`Record with ID ${id} not found in table ${tableName}`);
618
+ }
619
+ // Return the updated record
620
+ const updated = await this.getById(String(id), tableName);
621
+ if (!updated) {
622
+ throw new Error(`Failed to retrieve updated record ${id} from table ${tableName}`);
623
+ }
624
+ return updated;
625
+ }
626
+ // ========== Methods for CustomTableManager ==========
627
+ async findByConditions(tableName, conditions) {
628
+ this.#validateTableName(tableName);
629
+ let query = this.#knex(tableName).select('*');
630
+ // Apply each condition
631
+ for (const [column, value] of Object.entries(conditions)) {
632
+ if (value === null) {
633
+ query = query.whereNull(column);
634
+ }
635
+ else if (value === undefined) {
636
+ // Skip undefined values
637
+ continue;
638
+ }
639
+ else {
640
+ query = query.where(column, value);
641
+ }
642
+ }
643
+ // Check if table has 'date' column, otherwise use 'created_at'
644
+ const hasDateColumn = await this.#knex.schema.hasColumn(tableName, 'date');
645
+ const sortColumn = hasDateColumn ? 'date' : 'created_at';
646
+ const rows = await query.orderBy(sortColumn, 'desc');
647
+ return rows.map(row => mapToDataRecord(row, this.#storage));
648
+ }
649
+ async updateById(tableName, id, data) {
650
+ this.#validateTableName(tableName);
651
+ // Create a clean update object with updated_at timestamp
652
+ const updateData = {
653
+ ...data,
654
+ updated_at: new Date()
655
+ };
656
+ // Remove system fields that shouldn't be updated
657
+ delete updateData.id;
658
+ delete updateData.created_at;
659
+ delete updateData.date;
660
+ // Serialize file_index to JSON string if present (stored as TEXT in DB)
661
+ if ('file_index' in updateData && updateData.file_index) {
662
+ updateData.file_index =
663
+ typeof updateData.file_index === 'string'
664
+ ? updateData.file_index
665
+ : JSON.stringify(updateData.file_index);
666
+ }
667
+ const rowsAffected = await this.#knex(tableName).where({ id }).update(updateData);
668
+ if (rowsAffected === 0) {
669
+ throw new Error(`No record found with ID ${id} in table ${tableName}`);
670
+ }
671
+ }
672
+ async close() {
673
+ await this.#knex.destroy();
674
+ }
675
+ /**
676
+ * Find records for custom tables (returns raw database rows, not DataRecords)
677
+ * This bypasses mapToDataRecord() which assumes standard table structure
678
+ */
679
+ async findCustomTableRecords(tableName, conditions = {}) {
680
+ this.#validateTableName(tableName);
681
+ let query = this.#knex(tableName).select('*');
682
+ // Apply each condition
683
+ for (const [column, value] of Object.entries(conditions)) {
684
+ if (value === null) {
685
+ query = query.whereNull(column);
686
+ }
687
+ else if (value === undefined) {
688
+ // Skip undefined values
689
+ continue;
690
+ }
691
+ else {
692
+ query = query.where(column, value);
693
+ }
694
+ }
695
+ // Always sort by created_at for custom tables
696
+ const rows = await query.orderBy('created_at', 'desc');
697
+ return rows;
698
+ }
699
+ /**
700
+ * Get a single custom table record by ID (returns raw database row, not DataRecord)
701
+ */
702
+ async getCustomTableRecordById(tableName, id) {
703
+ this.#validateTableName(tableName);
704
+ const row = await this.#knex(tableName).where({ id }).first();
705
+ return row || null;
706
+ }
707
+ /**
708
+ * Insert a record into a custom table (returns the new record ID)
709
+ */
710
+ async insertCustomTableRecord(tableName, data) {
711
+ this.#validateTableName(tableName);
712
+ const now = new Date();
713
+ const insertData = {
714
+ ...data,
715
+ created_at: now,
716
+ updated_at: now
717
+ };
718
+ const result = await this.#knex(tableName).insert(insertData).returning('id');
719
+ const insertedId = result[0];
720
+ return typeof insertedId === 'object' ? insertedId.id : insertedId;
721
+ }
722
+ /**
723
+ * Get the underlying Knex instance for advanced operations
724
+ */
725
+ getKnex() {
726
+ return this.#knex;
727
+ }
728
+ }
729
+ //# sourceMappingURL=knex_database_adapter.js.map