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.
- package/LICENSE +20 -20
- package/README.md +494 -359
- package/dist/auth/apisix_parser.d.ts +141 -0
- package/dist/auth/apisix_parser.d.ts.map +1 -0
- package/dist/auth/apisix_parser.js +161 -0
- package/dist/auth/apisix_parser.js.map +1 -0
- package/dist/auth/auth_config.d.ts +126 -0
- package/dist/auth/auth_config.d.ts.map +1 -0
- package/dist/auth/auth_config.js +169 -0
- package/dist/auth/auth_config.js.map +1 -0
- package/dist/auth/auth_provider.d.ts +118 -0
- package/dist/auth/auth_provider.d.ts.map +1 -0
- package/dist/auth/auth_provider.js +8 -0
- package/dist/auth/auth_provider.js.map +1 -0
- package/dist/auth/auth_provider_factory.d.ts +91 -0
- package/dist/auth/auth_provider_factory.d.ts.map +1 -0
- package/dist/auth/auth_provider_factory.js +146 -0
- package/dist/auth/auth_provider_factory.js.map +1 -0
- package/dist/auth/index.d.ts +8 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +7 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/providers/gateway_auth_provider.d.ts +78 -0
- package/dist/auth/providers/gateway_auth_provider.d.ts.map +1 -0
- package/dist/auth/providers/gateway_auth_provider.js +109 -0
- package/dist/auth/providers/gateway_auth_provider.js.map +1 -0
- package/dist/auth/providers/index.d.ts +4 -0
- package/dist/auth/providers/index.d.ts.map +1 -0
- package/dist/auth/providers/index.js +4 -0
- package/dist/auth/providers/index.js.map +1 -0
- package/dist/auth/providers/jwt_auth_provider.d.ts +91 -0
- package/dist/auth/providers/jwt_auth_provider.d.ts.map +1 -0
- package/dist/auth/providers/jwt_auth_provider.js +204 -0
- package/dist/auth/providers/jwt_auth_provider.js.map +1 -0
- package/dist/auth/providers/no_auth_provider.d.ts +61 -0
- package/dist/auth/providers/no_auth_provider.d.ts.map +1 -0
- package/dist/auth/providers/no_auth_provider.js +76 -0
- package/dist/auth/providers/no_auth_provider.js.map +1 -0
- package/dist/auth/types.d.ts +100 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +2 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/auth/user_service.d.ts +86 -0
- package/dist/auth/user_service.d.ts.map +1 -0
- package/dist/auth/user_service.js +237 -0
- package/dist/auth/user_service.js.map +1 -0
- package/dist/components/assets_manager.d.ts +662 -0
- package/dist/components/assets_manager.d.ts.map +1 -0
- package/dist/components/assets_manager.js +1537 -0
- package/dist/components/assets_manager.js.map +1 -0
- package/dist/components/async_upload.d.ts +20 -0
- package/dist/components/async_upload.d.ts.map +1 -0
- package/dist/components/async_upload.js +10 -0
- package/dist/components/async_upload.js.map +1 -0
- package/dist/components/collector.d.ts +203 -0
- package/dist/components/collector.d.ts.map +1 -0
- package/dist/components/collector.js +214 -0
- package/dist/components/collector.js.map +1 -0
- package/dist/components/custom_table_manager.d.ts +503 -0
- package/dist/components/custom_table_manager.d.ts.map +1 -0
- package/dist/components/custom_table_manager.js +1023 -0
- package/dist/components/custom_table_manager.js.map +1 -0
- package/dist/components/global_assets_handler.d.ts +63 -0
- package/dist/components/global_assets_handler.d.ts.map +1 -0
- package/dist/components/global_assets_handler.js +127 -0
- package/dist/components/global_assets_handler.js.map +1 -0
- package/dist/components/handler.d.ts +104 -0
- package/dist/components/handler.d.ts.map +1 -0
- package/dist/components/handler.js +110 -0
- package/dist/components/handler.js.map +1 -0
- package/dist/components/harvester.d.ts +182 -0
- package/dist/components/harvester.d.ts.map +1 -0
- package/dist/components/harvester.js +406 -0
- package/dist/components/harvester.js.map +1 -0
- package/dist/components/index.d.ts +11 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +9 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/interfaces.d.ts +126 -0
- package/dist/components/interfaces.d.ts.map +1 -0
- package/dist/components/interfaces.js +8 -0
- package/dist/components/interfaces.js.map +1 -0
- package/dist/components/map_manager.d.ts +61 -0
- package/dist/components/map_manager.d.ts.map +1 -0
- package/dist/components/map_manager.js +242 -0
- package/dist/components/map_manager.js.map +1 -0
- package/dist/components/tileset_manager.d.ts +125 -0
- package/dist/components/tileset_manager.d.ts.map +1 -0
- package/dist/components/tileset_manager.js +623 -0
- package/dist/components/tileset_manager.js.map +1 -0
- package/dist/components/types.d.ts +226 -0
- package/dist/components/types.d.ts.map +1 -0
- package/dist/components/types.js +8 -0
- package/dist/components/types.js.map +1 -0
- package/dist/database/adapters/knex_database_adapter.d.ts +97 -0
- package/dist/database/adapters/knex_database_adapter.d.ts.map +1 -0
- package/dist/database/adapters/knex_database_adapter.js +729 -0
- package/dist/database/adapters/knex_database_adapter.js.map +1 -0
- package/dist/database/database_adapter.d.ts +262 -0
- package/dist/database/database_adapter.d.ts.map +1 -0
- package/dist/database/database_adapter.js +46 -0
- package/dist/database/database_adapter.js.map +1 -0
- package/dist/engine/digital_twin_engine.d.ts +295 -0
- package/dist/engine/digital_twin_engine.d.ts.map +1 -0
- package/dist/engine/digital_twin_engine.js +907 -0
- package/dist/engine/digital_twin_engine.js.map +1 -0
- package/dist/engine/endpoints.d.ts +47 -0
- package/dist/engine/endpoints.d.ts.map +1 -0
- package/dist/engine/endpoints.js +88 -0
- package/dist/engine/endpoints.js.map +1 -0
- package/dist/engine/error_handler.d.ts +20 -0
- package/dist/engine/error_handler.d.ts.map +1 -0
- package/dist/engine/error_handler.js +69 -0
- package/dist/engine/error_handler.js.map +1 -0
- package/dist/engine/events.d.ts +93 -0
- package/dist/engine/events.d.ts.map +1 -0
- package/dist/engine/events.js +71 -0
- package/dist/engine/events.js.map +1 -0
- package/dist/engine/health.d.ts +112 -0
- package/dist/engine/health.d.ts.map +1 -0
- package/dist/engine/health.js +190 -0
- package/dist/engine/health.js.map +1 -0
- package/dist/engine/initializer.d.ts +62 -0
- package/dist/engine/initializer.d.ts.map +1 -0
- package/dist/engine/initializer.js +108 -0
- package/dist/engine/initializer.js.map +1 -0
- package/dist/engine/queue_manager.d.ts +87 -0
- package/dist/engine/queue_manager.d.ts.map +1 -0
- package/dist/engine/queue_manager.js +196 -0
- package/dist/engine/queue_manager.js.map +1 -0
- package/dist/engine/scheduler.d.ts +30 -0
- package/dist/engine/scheduler.d.ts.map +1 -0
- package/dist/engine/scheduler.js +378 -0
- package/dist/engine/scheduler.js.map +1 -0
- package/dist/engine/upload_processor.d.ts +36 -0
- package/dist/engine/upload_processor.d.ts.map +1 -0
- package/dist/engine/upload_processor.js +113 -0
- package/dist/engine/upload_processor.js.map +1 -0
- package/dist/env/env.d.ts +134 -0
- package/dist/env/env.d.ts.map +1 -0
- package/dist/env/env.js +177 -0
- package/dist/env/env.js.map +1 -0
- package/dist/errors/index.d.ts +94 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +149 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/openapi/generator.d.ts +93 -0
- package/dist/openapi/generator.d.ts.map +1 -0
- package/dist/openapi/generator.js +293 -0
- package/dist/openapi/generator.js.map +1 -0
- package/dist/openapi/index.d.ts +9 -0
- package/dist/openapi/index.d.ts.map +1 -0
- package/dist/openapi/index.js +9 -0
- package/dist/openapi/index.js.map +1 -0
- package/dist/openapi/types.d.ts +182 -0
- package/dist/openapi/types.d.ts.map +1 -0
- package/dist/openapi/types.js +16 -0
- package/dist/openapi/types.js.map +1 -0
- package/dist/storage/adapters/local_storage_service.d.ts +57 -0
- package/dist/storage/adapters/local_storage_service.d.ts.map +1 -0
- package/dist/storage/adapters/local_storage_service.js +132 -0
- package/dist/storage/adapters/local_storage_service.js.map +1 -0
- package/dist/storage/adapters/ovh_storage_service.d.ts +72 -0
- package/dist/storage/adapters/ovh_storage_service.d.ts.map +1 -0
- package/dist/storage/adapters/ovh_storage_service.js +205 -0
- package/dist/storage/adapters/ovh_storage_service.js.map +1 -0
- package/dist/storage/storage_factory.d.ts +14 -0
- package/dist/storage/storage_factory.d.ts.map +1 -0
- package/dist/storage/storage_factory.js +43 -0
- package/dist/storage/storage_factory.js.map +1 -0
- package/dist/storage/storage_service.d.ts +163 -0
- package/dist/storage/storage_service.d.ts.map +1 -0
- package/dist/storage/storage_service.js +58 -0
- package/dist/storage/storage_service.js.map +1 -0
- package/dist/types/data_record.d.ts +123 -0
- package/dist/types/data_record.d.ts.map +1 -0
- package/dist/types/data_record.js +8 -0
- package/dist/types/data_record.js.map +1 -0
- package/dist/utils/graceful_shutdown.d.ts +44 -0
- package/dist/utils/graceful_shutdown.d.ts.map +1 -0
- package/dist/utils/graceful_shutdown.js +79 -0
- package/dist/utils/graceful_shutdown.js.map +1 -0
- package/dist/utils/http_responses.d.ts +175 -0
- package/dist/utils/http_responses.d.ts.map +1 -0
- package/dist/utils/http_responses.js +216 -0
- package/dist/utils/http_responses.js.map +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +74 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +92 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/map_to_data_record.d.ts +10 -0
- package/dist/utils/map_to_data_record.d.ts.map +1 -0
- package/dist/utils/map_to_data_record.js +36 -0
- package/dist/utils/map_to_data_record.js.map +1 -0
- package/dist/utils/safe_async.d.ts +50 -0
- package/dist/utils/safe_async.d.ts.map +1 -0
- package/dist/utils/safe_async.js +90 -0
- package/dist/utils/safe_async.js.map +1 -0
- package/dist/utils/servable_endpoint.d.ts +63 -0
- package/dist/utils/servable_endpoint.d.ts.map +1 -0
- package/dist/utils/servable_endpoint.js +67 -0
- package/dist/utils/servable_endpoint.js.map +1 -0
- package/dist/utils/zip_utils.d.ts +66 -0
- package/dist/utils/zip_utils.d.ts.map +1 -0
- package/dist/utils/zip_utils.js +169 -0
- package/dist/utils/zip_utils.js.map +1 -0
- package/dist/validation/index.d.ts +3 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +7 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/validation/schemas.d.ts +273 -0
- package/dist/validation/schemas.d.ts.map +1 -0
- package/dist/validation/schemas.js +82 -0
- package/dist/validation/schemas.js.map +1 -0
- package/dist/validation/validate.d.ts +49 -0
- package/dist/validation/validate.d.ts.map +1 -0
- package/dist/validation/validate.js +110 -0
- package/dist/validation/validate.js.map +1 -0
- 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
|