@camstack/core 0.1.3 → 0.1.4
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/dist/builtins/local-backup/index.mjs +10 -3
- package/dist/builtins/local-backup/index.mjs.map +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +5 -3
- package/dist/builtins/sqlite-storage/index.js +197 -95
- package/dist/builtins/sqlite-storage/index.js.map +1 -1
- package/dist/builtins/sqlite-storage/index.mjs +26 -7
- package/dist/builtins/sqlite-storage/index.mjs.map +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +4 -2
- package/dist/builtins/winston-logging/index.mjs +10 -3
- package/dist/builtins/winston-logging/index.mjs.map +1 -1
- package/dist/{chunk-QEMJH3KY.mjs → chunk-4JEXNFZZ.mjs} +11 -2
- package/dist/chunk-4YD6WMO6.mjs +207 -0
- package/dist/{chunk-SPA4JBKN.mjs.map → chunk-4YD6WMO6.mjs.map} +1 -1
- package/dist/chunk-CHFIH4G6.mjs +314 -0
- package/dist/{chunk-YXNXYYHL.mjs.map → chunk-CHFIH4G6.mjs.map} +1 -1
- package/dist/chunk-EFQ25JFE.mjs +689 -0
- package/dist/chunk-EFQ25JFE.mjs.map +1 -0
- package/dist/chunk-GBWW3JU4.mjs +180 -0
- package/dist/{chunk-SO4LROOT.mjs.map → chunk-GBWW3JU4.mjs.map} +1 -1
- package/dist/chunk-XSLBW5C2.mjs +177 -0
- package/dist/{chunk-LQFPAEQF.mjs.map → chunk-XSLBW5C2.mjs.map} +1 -1
- package/dist/index.js +14872 -11586
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +16119 -5933
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/dist/chunk-2F3XZYRW.mjs +0 -89
- package/dist/chunk-2F3XZYRW.mjs.map +0 -1
- package/dist/chunk-LQFPAEQF.mjs +0 -147
- package/dist/chunk-R3DIIBBX.mjs +0 -532
- package/dist/chunk-R3DIIBBX.mjs.map +0 -1
- package/dist/chunk-SO4LROOT.mjs +0 -150
- package/dist/chunk-SPA4JBKN.mjs +0 -175
- package/dist/chunk-YXNXYYHL.mjs +0 -282
- package/dist/dist-N7SR63RN.mjs +0 -3515
- package/dist/dist-N7SR63RN.mjs.map +0 -1
- package/dist/storage-location-manager-UQRGHTCA.mjs +0 -8
- package/dist/storage-location-manager-UQRGHTCA.mjs.map +0 -1
- package/dist/wrapper-Y55ADNM5.mjs +0 -3652
- package/dist/wrapper-Y55ADNM5.mjs.map +0 -1
- /package/dist/{chunk-QEMJH3KY.mjs.map → chunk-4JEXNFZZ.mjs.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/builtins/sqlite-storage/sqlite-storage.provider.ts","../src/builtins/sqlite-storage/sqlite-storage.addon.ts","../src/builtins/sqlite-storage/sql-schema.ts","../src/builtins/sqlite-storage/settings-store.ts","../src/builtins/sqlite-storage/sql-schema.ts"],"sourcesContent":["import Database from 'better-sqlite3'\nimport * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport { randomUUID } from 'node:crypto'\nimport type {\n IStorageProvider,\n IStorageLocation,\n IStructuredStorage,\n IFileStorage,\n StorageLocationName,\n StorageRecord,\n QueryFilter,\n} from '@camstack/types'\n\ntype LocationType = 'structured' | 'files' | 'both'\n\n/** Which locations need structured (SQL) storage, files storage, or both.\n * All structured locations share a SINGLE SQLite database (camstack.db).\n * File locations use the filesystem at their configured path. */\nconst LOCATION_TYPES: Record<string, LocationType> = {\n // New location names (from StorageLocationManager)\n data: 'structured', // settings, events, trails — SQL only\n media: 'files', // crops, snapshots, thumbnails — files only\n recordings: 'files', // video segments — files only\n models: 'files', // ONNX/TFLite models — files only\n cache: 'files', // temp files — files only\n logs: 'files', // Winston log files — files only\n // Legacy location names (backward compat)\n config: 'structured',\n events: 'structured',\n addon: 'both',\n}\n\nclass SqliteStructuredStorage implements IStructuredStorage {\n private readonly ensuredTables = new Set<string>()\n\n constructor(private readonly db: Database.Database) {}\n\n private ensureTable(collection: string): void {\n if (this.ensuredTables.has(collection)) return\n this.db.exec(\n `CREATE TABLE IF NOT EXISTS \"${collection}\" (id TEXT PRIMARY KEY, data TEXT)`,\n )\n this.ensuredTables.add(collection)\n }\n\n async insert(record: StorageRecord): Promise<StorageRecord> {\n this.ensureTable(record.collection)\n const id = record.id || randomUUID()\n const newRecord: StorageRecord = {\n collection: record.collection,\n id,\n data: record.data,\n }\n this.db\n .prepare(`INSERT INTO \"${record.collection}\" (id, data) VALUES (?, ?)`)\n .run(id, JSON.stringify(newRecord.data))\n return newRecord\n }\n\n async query(collection: string, filter?: QueryFilter): Promise<readonly StorageRecord[]> {\n this.ensureTable(collection)\n const { sql, params } = this.buildSelect(collection, filter)\n const rows = this.db.prepare(sql).all(...params) as Array<{ id: string; data: string }>\n return rows.map((row) => ({\n collection,\n id: row.id,\n data: JSON.parse(row.data) as Record<string, unknown>,\n }))\n }\n\n async update(\n collection: string,\n id: string,\n data: Record<string, unknown>,\n ): Promise<StorageRecord> {\n this.ensureTable(collection)\n this.db\n .prepare(`UPDATE \"${collection}\" SET data = ? WHERE id = ?`)\n .run(JSON.stringify(data), id)\n return { collection, id, data }\n }\n\n async delete(collection: string, id: string): Promise<void> {\n this.ensureTable(collection)\n this.db.prepare(`DELETE FROM \"${collection}\" WHERE id = ?`).run(id)\n }\n\n async count(collection: string, filter?: QueryFilter): Promise<number> {\n this.ensureTable(collection)\n const { sql, params } = this.buildCount(collection, filter)\n const row = this.db.prepare(sql).get(...params) as { cnt: number }\n return row.cnt\n }\n\n private buildWhereClause(filter?: QueryFilter): { clause: string; params: unknown[] } {\n if (!filter) return { clause: '', params: [] }\n\n const conditions: string[] = []\n const params: unknown[] = []\n\n if (filter.where) {\n for (const [field, value] of Object.entries(filter.where)) {\n if (field === 'id') {\n conditions.push('id = ?')\n params.push(value)\n } else {\n conditions.push(`json_extract(data, '$.${field}') = ?`)\n params.push(value)\n }\n }\n }\n\n if (filter.whereIn) {\n for (const [field, values] of Object.entries(filter.whereIn)) {\n const placeholders = values.map(() => '?').join(', ')\n if (field === 'id') {\n conditions.push(`id IN (${placeholders})`)\n } else {\n conditions.push(`json_extract(data, '$.${field}') IN (${placeholders})`)\n }\n params.push(...values)\n }\n }\n\n if (filter.whereBetween) {\n for (const [field, [low, high]] of Object.entries(filter.whereBetween)) {\n if (field === 'id') {\n conditions.push('id BETWEEN ? AND ?')\n } else {\n conditions.push(`json_extract(data, '$.${field}') BETWEEN ? AND ?`)\n }\n params.push(low, high)\n }\n }\n\n const clause = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : ''\n return { clause, params }\n }\n\n private buildSelect(collection: string, filter?: QueryFilter): { sql: string; params: unknown[] } {\n const { clause, params } = this.buildWhereClause(filter)\n let sql = `SELECT id, data FROM \"${collection}\"${clause}`\n\n if (filter?.orderBy) {\n const dir = filter.orderBy.direction === 'desc' ? 'DESC' : 'ASC'\n if (filter.orderBy.field === 'id') {\n sql += ` ORDER BY id ${dir}`\n } else {\n sql += ` ORDER BY json_extract(data, '$.${filter.orderBy.field}') ${dir}`\n }\n }\n\n if (filter?.limit !== undefined) {\n sql += ` LIMIT ?`\n params.push(filter.limit)\n }\n\n if (filter?.offset !== undefined) {\n sql += ` OFFSET ?`\n params.push(filter.offset)\n }\n\n return { sql, params }\n }\n\n private buildCount(collection: string, filter?: QueryFilter): { sql: string; params: unknown[] } {\n const { clause, params } = this.buildWhereClause(filter)\n return { sql: `SELECT COUNT(*) as cnt FROM \"${collection}\"${clause}`, params }\n }\n}\n\nexport class FileSystemStorage implements IFileStorage {\n constructor(private readonly basePath: string) {}\n\n async readFile(filePath: string): Promise<Buffer> {\n const fullPath = path.join(this.basePath, filePath)\n return fs.promises.readFile(fullPath)\n }\n\n async writeFile(filePath: string, data: Buffer): Promise<void> {\n const fullPath = path.join(this.basePath, filePath)\n fs.mkdirSync(path.dirname(fullPath), { recursive: true })\n await fs.promises.writeFile(fullPath, data)\n }\n\n async deleteFile(filePath: string): Promise<void> {\n const fullPath = path.join(this.basePath, filePath)\n await fs.promises.unlink(fullPath)\n }\n\n async listFiles(prefix?: string): Promise<readonly string[]> {\n const searchDir = prefix ? path.join(this.basePath, prefix) : this.basePath\n try {\n const entries = await fs.promises.readdir(searchDir, { recursive: true })\n const files: string[] = []\n for (const entry of entries) {\n const entryStr = String(entry)\n const relative = prefix ? path.join(prefix, entryStr) : entryStr\n const fullPath = path.join(this.basePath, relative)\n try {\n const stat = await fs.promises.stat(fullPath)\n if (stat.isFile()) {\n files.push(relative)\n }\n } catch {\n // Skip entries that can't be stat'd\n }\n }\n return files\n } catch {\n return []\n }\n }\n\n async getFileUrl(_path: string): Promise<string> {\n return null as unknown as string\n }\n\n async exists(filePath: string): Promise<boolean> {\n const fullPath = path.join(this.basePath, filePath)\n try {\n await fs.promises.access(fullPath, fs.constants.F_OK)\n return true\n } catch {\n return false\n }\n }\n}\n\n/** @deprecated Use FilesystemStorageProvider + SqliteSettingsBackend instead */\nexport class SqliteStorageProvider {\n private mainDb: Database.Database | null = null\n private sharedStructured: SqliteStructuredStorage | null = null\n private readonly locations = new Map<StorageLocationName, IStorageLocation>()\n\n async initialize(): Promise<void> {\n // Called by interface contract; actual setup done via configure()\n }\n\n /**\n * Configure all storage locations.\n * ONE single SQLite database (camstack.db) is used for ALL structured storage.\n * File-based locations use the filesystem at their configured path.\n */\n async configure(config: { locations: Record<string, string> }): Promise<void> {\n // Find the 'data' location path for the single DB (fall back to first structured location)\n const dataPath = config.locations['data'] ?? config.locations['config'] ?? Object.values(config.locations)[0]\n if (!dataPath) throw new Error('No data path configured for SQLite storage')\n\n // Create single database\n fs.mkdirSync(dataPath, { recursive: true })\n const dbPath = path.join(dataPath, 'camstack.db')\n this.mainDb = new Database(dbPath)\n this.mainDb.pragma('journal_mode = WAL')\n this.sharedStructured = new SqliteStructuredStorage(this.mainDb)\n\n // Configure each location\n for (const [name, dirPath] of Object.entries(config.locations)) {\n const locationName = name as StorageLocationName\n const locationType = LOCATION_TYPES[name] ?? 'files'\n\n fs.mkdirSync(dirPath, { recursive: true })\n\n const location: IStorageLocation = {}\n\n // All structured storage shares the single DB\n if (locationType === 'structured' || locationType === 'both') {\n location.structured = this.sharedStructured\n }\n\n if (locationType === 'files' || locationType === 'both') {\n location.files = new FileSystemStorage(dirPath)\n }\n\n this.locations.set(locationName, location)\n }\n }\n\n getLocation(name: StorageLocationName): IStorageLocation {\n const location = this.locations.get(name)\n if (!location) {\n throw new Error(`Storage location \"${name}\" not found`)\n }\n return location\n }\n\n async shutdown(): Promise<void> {\n if (this.mainDb) {\n this.mainDb.close()\n this.mainDb = null\n this.sharedStructured = null\n }\n this.locations.clear()\n }\n\n async export(_locationName: StorageLocationName): Promise<Buffer> {\n throw new Error('Export not yet implemented')\n }\n\n async import(_locationName: StorageLocationName, _data: Buffer): Promise<void> {\n throw new Error('Import not yet implemented')\n }\n}\n","import type {\n ICamstackAddon,\n AddonManifest,\n AddonContext,\n IConfigurable,\n ConfigUISchema,\n CapabilityProviderMap,\n} from '@camstack/types'\nimport { SqliteStorageProvider } from './sqlite-storage.provider'\n\nexport class SqliteStorageAddon implements ICamstackAddon, IConfigurable {\n readonly manifest: AddonManifest = {\n id: 'sqlite-storage',\n name: 'SQLite Storage',\n version: '1.0.0',\n capabilities: ['storage'],\n }\n\n private provider: SqliteStorageProvider | null = null\n\n async initialize(context: AddonContext): Promise<void> {\n const storageConfig = {\n locations: { ...context.locationPaths } as Record<string, string>,\n }\n this.provider = new SqliteStorageProvider()\n await this.provider.configure(storageConfig)\n context.logger.info('SQLite storage initialized')\n }\n\n async shutdown(): Promise<void> {\n await this.provider?.shutdown()\n }\n\n getProvider(): SqliteStorageProvider {\n if (!this.provider) throw new Error('SQLite storage not initialized')\n return this.provider\n }\n\n getCapabilityProvider<K extends keyof CapabilityProviderMap>(\n name: K,\n ): CapabilityProviderMap[K] | null {\n if (name === 'storage' && this.provider) {\n return this.provider as unknown as CapabilityProviderMap[K]\n }\n return null\n }\n\n getConfigSchema(): ConfigUISchema {\n return {\n sections: [],\n }\n }\n\n getConfig(): Record<string, unknown> {\n return {}\n }\n\n async onConfigChange(_config: Record<string, unknown>): Promise<void> {\n // No configurable fields\n }\n}\n","/** Core table DDL statements -- executed on first boot */\nexport const CORE_TABLE_DDL: readonly string[] = [\n // Settings tables\n `CREATE TABLE IF NOT EXISTS system_settings (\n key TEXT PRIMARY KEY,\n value JSON NOT NULL,\n updated_at INTEGER NOT NULL DEFAULT (unixepoch())\n )`,\n `CREATE TABLE IF NOT EXISTS addon_settings (\n addon_id TEXT NOT NULL,\n key TEXT NOT NULL,\n value JSON NOT NULL,\n updated_at INTEGER NOT NULL DEFAULT (unixepoch()),\n PRIMARY KEY (addon_id, key)\n )`,\n `CREATE TABLE IF NOT EXISTS provider_settings (\n provider_id TEXT NOT NULL,\n key TEXT NOT NULL,\n value JSON NOT NULL,\n updated_at INTEGER NOT NULL DEFAULT (unixepoch()),\n PRIMARY KEY (provider_id, key)\n )`,\n `CREATE TABLE IF NOT EXISTS device_settings (\n device_id TEXT NOT NULL,\n key TEXT NOT NULL,\n value JSON NOT NULL,\n updated_at INTEGER NOT NULL DEFAULT (unixepoch()),\n PRIMARY KEY (device_id, key)\n )`,\n\n // Detection events\n `CREATE TABLE IF NOT EXISTS detection_events (\n id TEXT PRIMARY KEY,\n timestamp INTEGER NOT NULL,\n device_id TEXT NOT NULL,\n class_name TEXT NOT NULL,\n score REAL NOT NULL,\n severity TEXT NOT NULL,\n track_id TEXT,\n zones JSON,\n recognition JSON,\n media_files JSON,\n data JSON\n )`,\n `CREATE INDEX IF NOT EXISTS idx_det_device_ts ON detection_events(device_id, timestamp)`,\n `CREATE INDEX IF NOT EXISTS idx_det_class_ts ON detection_events(class_name, timestamp)`,\n\n // Audio levels\n `CREATE TABLE IF NOT EXISTS audio_levels (\n id TEXT PRIMARY KEY,\n timestamp INTEGER NOT NULL,\n device_id TEXT NOT NULL,\n dbfs REAL NOT NULL,\n rms REAL NOT NULL,\n state TEXT NOT NULL\n )`,\n `CREATE INDEX IF NOT EXISTS idx_audio_device_ts ON audio_levels(device_id, timestamp)`,\n\n // Track trails\n `CREATE TABLE IF NOT EXISTS track_trails (\n track_id TEXT PRIMARY KEY,\n device_id TEXT NOT NULL,\n class_name TEXT NOT NULL,\n first_seen INTEGER NOT NULL,\n last_seen INTEGER NOT NULL,\n positions JSON NOT NULL,\n snapshots JSON,\n total_distance REAL,\n zones_visited JSON\n )`,\n `CREATE INDEX IF NOT EXISTS idx_trails_device_ts ON track_trails(device_id, first_seen)`,\n]\n\n/** Addon table schema declaration */\nexport interface AddonTableSchema {\n readonly name: string\n readonly columns: ReadonlyArray<{\n readonly name: string\n readonly type: 'TEXT' | 'INTEGER' | 'REAL' | 'JSON'\n readonly primaryKey?: boolean\n readonly notNull?: boolean\n }>\n readonly indexes?: ReadonlyArray<{\n readonly name: string\n readonly columns: readonly string[]\n readonly unique?: boolean\n }>\n}\n\n/** Generate CREATE TABLE DDL from addon schema */\nexport function addonTableToDdl(schema: AddonTableSchema): string[] {\n const pks = schema.columns.filter(c => c.primaryKey).map(c => c.name)\n const colDefs = schema.columns.map(c => {\n const parts = [c.name, c.type]\n if (c.notNull) parts.push('NOT NULL')\n return parts.join(' ')\n })\n\n let ddl = `CREATE TABLE IF NOT EXISTS ${schema.name} (\\n ${colDefs.join(',\\n ')}`\n if (pks.length > 0) {\n ddl += `,\\n PRIMARY KEY (${pks.join(', ')})`\n }\n ddl += '\\n)'\n\n const stmts = [ddl]\n for (const idx of schema.indexes ?? []) {\n const unique = idx.unique ? 'UNIQUE ' : ''\n stmts.push(`CREATE ${unique}INDEX IF NOT EXISTS ${idx.name} ON ${schema.name}(${idx.columns.join(', ')})`)\n }\n return stmts\n}\n","import Database from 'better-sqlite3'\nimport { CORE_TABLE_DDL } from './sql-schema.js'\nimport { RUNTIME_DEFAULTS } from '@camstack/kernel'\n\n/**\n * Thin wrapper over better-sqlite3 that manages the four settings tables:\n * system_settings, addon_settings, provider_settings, device_settings.\n *\n * All values are stored as JSON text and deserialized on read.\n */\nexport class SettingsStore {\n private readonly db: Database.Database\n\n constructor(dbPath: string) {\n this.db = new Database(dbPath)\n this.db.pragma('journal_mode = WAL')\n this.db.pragma('foreign_keys = ON')\n this.initTables()\n }\n\n // ---------------------------------------------------------------------------\n // System settings\n // ---------------------------------------------------------------------------\n\n getSystem(key: string): unknown {\n const row = this.db\n .prepare<[string], { value: string }>('SELECT value FROM system_settings WHERE key = ?')\n .get(key)\n if (row === undefined) return undefined\n return JSON.parse(row.value)\n }\n\n setSystem(key: string, value: unknown): void {\n this.db\n .prepare(\n `INSERT INTO system_settings (key, value, updated_at) VALUES (?, json(?), unixepoch())\n ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,\n )\n .run(key, JSON.stringify(value))\n }\n\n getAllSystem(): Record<string, unknown> {\n const rows = this.db\n .prepare<[], { key: string; value: string }>('SELECT key, value FROM system_settings')\n .all()\n return Object.fromEntries(rows.map(r => [r.key, JSON.parse(r.value)]))\n }\n\n // ---------------------------------------------------------------------------\n // Addon settings\n // ---------------------------------------------------------------------------\n\n getAddon(addonId: string, key: string): unknown {\n const row = this.db\n .prepare<[string, string], { value: string }>(\n 'SELECT value FROM addon_settings WHERE addon_id = ? AND key = ?',\n )\n .get(addonId, key)\n if (row === undefined) return undefined\n return JSON.parse(row.value)\n }\n\n setAddon(addonId: string, key: string, value: unknown): void {\n this.db\n .prepare(\n `INSERT INTO addon_settings (addon_id, key, value, updated_at) VALUES (?, ?, json(?), unixepoch())\n ON CONFLICT(addon_id, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,\n )\n .run(addonId, key, JSON.stringify(value))\n }\n\n getAllAddon(addonId: string): Record<string, unknown> {\n const rows = this.db\n .prepare<[string], { key: string; value: string }>(\n 'SELECT key, value FROM addon_settings WHERE addon_id = ?',\n )\n .all(addonId)\n return Object.fromEntries(rows.map(r => [r.key, JSON.parse(r.value)]))\n }\n\n /** Bulk-replace all keys for an addon (within a transaction). */\n setAllAddon(addonId: string, config: Record<string, unknown>): void {\n const deleteStmt = this.db.prepare('DELETE FROM addon_settings WHERE addon_id = ?')\n const insertStmt = this.db.prepare(\n `INSERT INTO addon_settings (addon_id, key, value, updated_at) VALUES (?, ?, json(?), unixepoch())`,\n )\n this.db.transaction(() => {\n deleteStmt.run(addonId)\n for (const [key, value] of Object.entries(config)) {\n insertStmt.run(addonId, key, JSON.stringify(value))\n }\n })()\n }\n\n // ---------------------------------------------------------------------------\n // Provider settings\n // ---------------------------------------------------------------------------\n\n getProvider(providerId: string, key: string): unknown {\n const row = this.db\n .prepare<[string, string], { value: string }>(\n 'SELECT value FROM provider_settings WHERE provider_id = ? AND key = ?',\n )\n .get(providerId, key)\n if (row === undefined) return undefined\n return JSON.parse(row.value)\n }\n\n setProvider(providerId: string, key: string, value: unknown): void {\n this.db\n .prepare(\n `INSERT INTO provider_settings (provider_id, key, value, updated_at) VALUES (?, ?, json(?), unixepoch())\n ON CONFLICT(provider_id, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,\n )\n .run(providerId, key, JSON.stringify(value))\n }\n\n getAllProvider(providerId: string): Record<string, unknown> {\n const rows = this.db\n .prepare<[string], { key: string; value: string }>(\n 'SELECT key, value FROM provider_settings WHERE provider_id = ?',\n )\n .all(providerId)\n return Object.fromEntries(rows.map(r => [r.key, JSON.parse(r.value)]))\n }\n\n // ---------------------------------------------------------------------------\n // Device settings\n // ---------------------------------------------------------------------------\n\n getDevice(deviceId: string, key: string): unknown {\n const row = this.db\n .prepare<[string, string], { value: string }>(\n 'SELECT value FROM device_settings WHERE device_id = ? AND key = ?',\n )\n .get(deviceId, key)\n if (row === undefined) return undefined\n return JSON.parse(row.value)\n }\n\n setDevice(deviceId: string, key: string, value: unknown): void {\n this.db\n .prepare(\n `INSERT INTO device_settings (device_id, key, value, updated_at) VALUES (?, ?, json(?), unixepoch())\n ON CONFLICT(device_id, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,\n )\n .run(deviceId, key, JSON.stringify(value))\n }\n\n getAllDevice(deviceId: string): Record<string, unknown> {\n const rows = this.db\n .prepare<[string], { key: string; value: string }>(\n 'SELECT key, value FROM device_settings WHERE device_id = ?',\n )\n .all(deviceId)\n return Object.fromEntries(rows.map(r => [r.key, JSON.parse(r.value)]))\n }\n\n // ---------------------------------------------------------------------------\n // Lifecycle\n // ---------------------------------------------------------------------------\n\n /** Close the SQLite connection (call on shutdown). */\n close(): void {\n this.db.close()\n }\n\n /** Check if system_settings is empty (used for first-boot seeding). */\n isSystemSettingsEmpty(): boolean {\n const row = this.db\n .prepare<[], { cnt: number }>('SELECT COUNT(*) AS cnt FROM system_settings')\n .get()\n return (row?.cnt ?? 0) === 0\n }\n\n /** Seed system_settings with RUNTIME_DEFAULTS (only on first boot). */\n seedDefaults(): void {\n const insert = this.db.prepare(\n `INSERT OR IGNORE INTO system_settings (key, value, updated_at) VALUES (?, json(?), unixepoch())`,\n )\n this.db.transaction(() => {\n for (const [key, value] of Object.entries(RUNTIME_DEFAULTS)) {\n insert.run(key, JSON.stringify(value))\n }\n })()\n }\n\n // ---------------------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------------------\n\n private initTables(): void {\n this.db.transaction(() => {\n for (const stmt of CORE_TABLE_DDL) {\n this.db.prepare(stmt).run()\n }\n })()\n }\n}\n","/** Core table DDL statements -- executed on first boot */\nexport const CORE_TABLE_DDL: readonly string[] = [\n // Settings tables\n `CREATE TABLE IF NOT EXISTS system_settings (\n key TEXT PRIMARY KEY,\n value JSON NOT NULL,\n updated_at INTEGER NOT NULL DEFAULT (unixepoch())\n )`,\n `CREATE TABLE IF NOT EXISTS addon_settings (\n addon_id TEXT NOT NULL,\n key TEXT NOT NULL,\n value JSON NOT NULL,\n updated_at INTEGER NOT NULL DEFAULT (unixepoch()),\n PRIMARY KEY (addon_id, key)\n )`,\n `CREATE TABLE IF NOT EXISTS provider_settings (\n provider_id TEXT NOT NULL,\n key TEXT NOT NULL,\n value JSON NOT NULL,\n updated_at INTEGER NOT NULL DEFAULT (unixepoch()),\n PRIMARY KEY (provider_id, key)\n )`,\n `CREATE TABLE IF NOT EXISTS device_settings (\n device_id TEXT NOT NULL,\n key TEXT NOT NULL,\n value JSON NOT NULL,\n updated_at INTEGER NOT NULL DEFAULT (unixepoch()),\n PRIMARY KEY (device_id, key)\n )`,\n\n // Detection events\n `CREATE TABLE IF NOT EXISTS detection_events (\n id TEXT PRIMARY KEY,\n timestamp INTEGER NOT NULL,\n device_id TEXT NOT NULL,\n class_name TEXT NOT NULL,\n score REAL NOT NULL,\n severity TEXT NOT NULL,\n track_id TEXT,\n zones JSON,\n recognition JSON,\n media_files JSON,\n data JSON\n )`,\n `CREATE INDEX IF NOT EXISTS idx_det_device_ts ON detection_events(device_id, timestamp)`,\n `CREATE INDEX IF NOT EXISTS idx_det_class_ts ON detection_events(class_name, timestamp)`,\n\n // Audio levels\n `CREATE TABLE IF NOT EXISTS audio_levels (\n id TEXT PRIMARY KEY,\n timestamp INTEGER NOT NULL,\n device_id TEXT NOT NULL,\n dbfs REAL NOT NULL,\n rms REAL NOT NULL,\n state TEXT NOT NULL\n )`,\n `CREATE INDEX IF NOT EXISTS idx_audio_device_ts ON audio_levels(device_id, timestamp)`,\n\n // Track trails\n `CREATE TABLE IF NOT EXISTS track_trails (\n track_id TEXT PRIMARY KEY,\n device_id TEXT NOT NULL,\n class_name TEXT NOT NULL,\n first_seen INTEGER NOT NULL,\n last_seen INTEGER NOT NULL,\n positions JSON NOT NULL,\n snapshots JSON,\n total_distance REAL,\n zones_visited JSON\n )`,\n `CREATE INDEX IF NOT EXISTS idx_trails_device_ts ON track_trails(device_id, first_seen)`,\n]\n\n/** Addon table schema declaration */\nexport interface AddonTableSchema {\n readonly name: string\n readonly columns: ReadonlyArray<{\n readonly name: string\n readonly type: 'TEXT' | 'INTEGER' | 'REAL' | 'JSON'\n readonly primaryKey?: boolean\n readonly notNull?: boolean\n }>\n readonly indexes?: ReadonlyArray<{\n readonly name: string\n readonly columns: readonly string[]\n readonly unique?: boolean\n }>\n}\n\n/** Generate CREATE TABLE DDL from addon schema */\nexport function addonTableToDdl(schema: AddonTableSchema): string[] {\n const pks = schema.columns.filter(c => c.primaryKey).map(c => c.name)\n const colDefs = schema.columns.map(c => {\n const parts = [c.name, c.type]\n if (c.notNull) parts.push('NOT NULL')\n return parts.join(' ')\n })\n\n let ddl = `CREATE TABLE IF NOT EXISTS ${schema.name} (\\n ${colDefs.join(',\\n ')}`\n if (pks.length > 0) {\n ddl += `,\\n PRIMARY KEY (${pks.join(', ')})`\n }\n ddl += '\\n)'\n\n const stmts = [ddl]\n for (const idx of schema.indexes ?? []) {\n const unique = idx.unique ? 'UNIQUE ' : ''\n stmts.push(`CREATE ${unique}INDEX IF NOT EXISTS ${idx.name} ON ${schema.name}(${idx.columns.join(', ')})`)\n }\n return stmts\n}\n"],"mappings":"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","names":["addonTableToDdl","Database","CORE_TABLE_DDL"]}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__esm,
|
|
3
|
+
__export
|
|
4
|
+
} from "./chunk-4JEXNFZZ.mjs";
|
|
5
|
+
|
|
6
|
+
// src/builtins/local-backup/local-backup.ts
|
|
7
|
+
var local_backup_exports = {};
|
|
8
|
+
__export(local_backup_exports, {
|
|
9
|
+
LocalBackupService: () => LocalBackupService
|
|
10
|
+
});
|
|
11
|
+
import { randomUUID } from "crypto";
|
|
12
|
+
var LocalBackupService;
|
|
13
|
+
var init_local_backup = __esm({
|
|
14
|
+
"src/builtins/local-backup/local-backup.ts"() {
|
|
15
|
+
"use strict";
|
|
16
|
+
LocalBackupService = class {
|
|
17
|
+
constructor(config, logger, eventBus, storage) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
this.logger = logger;
|
|
20
|
+
this.eventBus = eventBus;
|
|
21
|
+
this.storage = storage;
|
|
22
|
+
}
|
|
23
|
+
manifests = [];
|
|
24
|
+
/** Create a backup of specified locations */
|
|
25
|
+
async backup(options) {
|
|
26
|
+
const id = randomUUID();
|
|
27
|
+
const timestamp = Date.now();
|
|
28
|
+
const locations = options?.locations ?? ["config", "events", "logs"];
|
|
29
|
+
this.logger.info(`Starting backup ${id} (${locations.join(", ")})`);
|
|
30
|
+
const manifest = {
|
|
31
|
+
id,
|
|
32
|
+
timestamp,
|
|
33
|
+
label: options?.label,
|
|
34
|
+
locations,
|
|
35
|
+
sizeMB: 0,
|
|
36
|
+
path: `${this.config.backupDir}/${id}`
|
|
37
|
+
};
|
|
38
|
+
const updated = [...this.manifests, manifest];
|
|
39
|
+
this.manifests = updated;
|
|
40
|
+
await this.pruneOldBackups();
|
|
41
|
+
this.eventBus.emit({
|
|
42
|
+
id: randomUUID(),
|
|
43
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
44
|
+
source: { type: "addon", id: "local-backup" },
|
|
45
|
+
category: "backup.completed",
|
|
46
|
+
data: { backupId: id, locations: [...locations], sizeMB: manifest.sizeMB }
|
|
47
|
+
});
|
|
48
|
+
this.logger.info(`Backup ${id} completed`);
|
|
49
|
+
return manifest;
|
|
50
|
+
}
|
|
51
|
+
/** Restore from a backup */
|
|
52
|
+
async restore(backupId) {
|
|
53
|
+
const manifest = this.manifests.find((m) => m.id === backupId);
|
|
54
|
+
if (!manifest) throw new Error(`Backup ${backupId} not found`);
|
|
55
|
+
this.logger.info(`Restoring from backup ${backupId}`);
|
|
56
|
+
this.eventBus.emit({
|
|
57
|
+
id: randomUUID(),
|
|
58
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
59
|
+
source: { type: "addon", id: "local-backup" },
|
|
60
|
+
category: "backup.restored",
|
|
61
|
+
data: { backupId }
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/** List all backups sorted by timestamp descending */
|
|
65
|
+
list() {
|
|
66
|
+
return [...this.manifests].sort((a, b) => b.timestamp - a.timestamp);
|
|
67
|
+
}
|
|
68
|
+
/** Delete a specific backup */
|
|
69
|
+
async delete(backupId) {
|
|
70
|
+
this.manifests = this.manifests.filter((m) => m.id !== backupId);
|
|
71
|
+
}
|
|
72
|
+
async pruneOldBackups() {
|
|
73
|
+
const sorted = [...this.manifests].sort((a, b) => a.timestamp - b.timestamp);
|
|
74
|
+
while (sorted.length > this.config.retentionCount) {
|
|
75
|
+
const oldest = sorted.shift();
|
|
76
|
+
if (oldest) {
|
|
77
|
+
this.logger.info(`Pruning old backup ${oldest.id}`);
|
|
78
|
+
this.manifests = this.manifests.filter((m) => m.id !== oldest.id);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// src/builtins/local-backup/local-backup.addon.ts
|
|
87
|
+
var local_backup_addon_exports = {};
|
|
88
|
+
__export(local_backup_addon_exports, {
|
|
89
|
+
LocalBackupAddon: () => LocalBackupAddon
|
|
90
|
+
});
|
|
91
|
+
import * as path from "path";
|
|
92
|
+
var LocalBackupAddon;
|
|
93
|
+
var init_local_backup_addon = __esm({
|
|
94
|
+
"src/builtins/local-backup/local-backup.addon.ts"() {
|
|
95
|
+
"use strict";
|
|
96
|
+
init_local_backup();
|
|
97
|
+
LocalBackupAddon = class {
|
|
98
|
+
manifest = {
|
|
99
|
+
id: "local-backup",
|
|
100
|
+
name: "Local Backup",
|
|
101
|
+
version: "1.0.0",
|
|
102
|
+
capabilities: ["backup"]
|
|
103
|
+
};
|
|
104
|
+
service = null;
|
|
105
|
+
currentConfig = {
|
|
106
|
+
retentionCount: 7
|
|
107
|
+
};
|
|
108
|
+
async initialize(context) {
|
|
109
|
+
this.currentConfig = {
|
|
110
|
+
retentionCount: context.addonConfig.retentionCount ?? this.currentConfig.retentionCount
|
|
111
|
+
};
|
|
112
|
+
const backupConfig = {
|
|
113
|
+
backupDir: path.join(context.locationPaths.data, "backups"),
|
|
114
|
+
retentionCount: this.currentConfig.retentionCount
|
|
115
|
+
};
|
|
116
|
+
this.service = new LocalBackupService(
|
|
117
|
+
backupConfig,
|
|
118
|
+
context.logger,
|
|
119
|
+
context.eventBus,
|
|
120
|
+
context.storage
|
|
121
|
+
);
|
|
122
|
+
context.logger.info("Local Backup initialized");
|
|
123
|
+
}
|
|
124
|
+
async shutdown() {
|
|
125
|
+
this.service = null;
|
|
126
|
+
}
|
|
127
|
+
getService() {
|
|
128
|
+
if (!this.service) throw new Error("Local Backup not initialized");
|
|
129
|
+
return this.service;
|
|
130
|
+
}
|
|
131
|
+
getCapabilityProvider(name) {
|
|
132
|
+
if (name === "backup" && this.service) {
|
|
133
|
+
return this.service;
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
getConfigSchema() {
|
|
138
|
+
return {
|
|
139
|
+
sections: [
|
|
140
|
+
{
|
|
141
|
+
id: "backup-retention",
|
|
142
|
+
title: "Backup Retention",
|
|
143
|
+
description: "How many local backup snapshots to keep on disk.",
|
|
144
|
+
columns: 1,
|
|
145
|
+
fields: [
|
|
146
|
+
{
|
|
147
|
+
type: "number",
|
|
148
|
+
key: "retentionCount",
|
|
149
|
+
label: "Retention Count",
|
|
150
|
+
description: "Number of backup snapshots to keep before deleting the oldest",
|
|
151
|
+
min: 1,
|
|
152
|
+
max: 100,
|
|
153
|
+
step: 1
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
getConfig() {
|
|
161
|
+
return { ...this.currentConfig };
|
|
162
|
+
}
|
|
163
|
+
async onConfigChange(config) {
|
|
164
|
+
this.currentConfig = {
|
|
165
|
+
retentionCount: config.retentionCount ?? this.currentConfig.retentionCount
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
export {
|
|
173
|
+
LocalBackupService,
|
|
174
|
+
local_backup_exports,
|
|
175
|
+
init_local_backup,
|
|
176
|
+
LocalBackupAddon,
|
|
177
|
+
local_backup_addon_exports,
|
|
178
|
+
init_local_backup_addon
|
|
179
|
+
};
|
|
180
|
+
//# sourceMappingURL=chunk-GBWW3JU4.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/builtins/local-backup/local-backup.ts","../src/builtins/local-backup/local-backup.addon.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\nimport type { IScopedLogger, IEventBus, IStorageLocation, BackupManifest } from '@camstack/types'\n\nexport type { BackupManifest }\n\nexport interface BackupConfig {\n readonly backupDir: string\n readonly retentionCount: number\n}\n\nexport class LocalBackupService {\n private manifests: BackupManifest[] = []\n\n constructor(\n private readonly config: BackupConfig,\n private readonly logger: IScopedLogger,\n private readonly eventBus: IEventBus,\n private readonly storage: IStorageLocation,\n ) {}\n\n /** Create a backup of specified locations */\n async backup(options?: {\n locations?: string[]\n label?: string\n }): Promise<BackupManifest> {\n const id = randomUUID()\n const timestamp = Date.now()\n const locations = options?.locations ?? ['config', 'events', 'logs']\n\n this.logger.info(`Starting backup ${id} (${locations.join(', ')})`)\n\n const manifest: BackupManifest = {\n id,\n timestamp,\n label: options?.label,\n locations,\n sizeMB: 0,\n path: `${this.config.backupDir}/${id}`,\n }\n\n const updated = [...this.manifests, manifest]\n this.manifests = updated\n\n await this.pruneOldBackups()\n\n this.eventBus.emit({\n id: randomUUID(),\n timestamp: new Date(),\n source: { type: 'addon', id: 'local-backup' },\n category: 'backup.completed',\n data: { backupId: id, locations: [...locations], sizeMB: manifest.sizeMB },\n })\n\n this.logger.info(`Backup ${id} completed`)\n return manifest\n }\n\n /** Restore from a backup */\n async restore(backupId: string): Promise<void> {\n const manifest = this.manifests.find((m) => m.id === backupId)\n if (!manifest) throw new Error(`Backup ${backupId} not found`)\n\n this.logger.info(`Restoring from backup ${backupId}`)\n\n this.eventBus.emit({\n id: randomUUID(),\n timestamp: new Date(),\n source: { type: 'addon', id: 'local-backup' },\n category: 'backup.restored',\n data: { backupId },\n })\n }\n\n /** List all backups sorted by timestamp descending */\n list(): readonly BackupManifest[] {\n return [...this.manifests].sort((a, b) => b.timestamp - a.timestamp)\n }\n\n /** Delete a specific backup */\n async delete(backupId: string): Promise<void> {\n this.manifests = this.manifests.filter((m) => m.id !== backupId)\n }\n\n private async pruneOldBackups(): Promise<void> {\n const sorted = [...this.manifests].sort((a, b) => a.timestamp - b.timestamp)\n\n while (sorted.length > this.config.retentionCount) {\n const oldest = sorted.shift()\n if (oldest) {\n this.logger.info(`Pruning old backup ${oldest.id}`)\n this.manifests = this.manifests.filter((m) => m.id !== oldest.id)\n }\n }\n }\n}\n","import * as path from 'node:path'\nimport type {\n ICamstackAddon, AddonManifest, AddonContext,\n IConfigurable, ConfigUISchema, CapabilityProviderMap,\n} from '@camstack/types'\nimport { LocalBackupService } from './local-backup'\nimport type { BackupConfig } from './local-backup'\n\nexport class LocalBackupAddon implements ICamstackAddon, IConfigurable {\n readonly manifest: AddonManifest = {\n id: 'local-backup',\n name: 'Local Backup',\n version: '1.0.0',\n capabilities: ['backup'],\n }\n\n private service: LocalBackupService | null = null\n private currentConfig = {\n retentionCount: 7,\n }\n\n async initialize(context: AddonContext): Promise<void> {\n this.currentConfig = {\n retentionCount: (context.addonConfig.retentionCount as number) ?? this.currentConfig.retentionCount,\n }\n const backupConfig: BackupConfig = {\n backupDir: path.join(context.locationPaths.data, 'backups'),\n retentionCount: this.currentConfig.retentionCount,\n }\n this.service = new LocalBackupService(\n backupConfig,\n context.logger,\n context.eventBus,\n context.storage,\n )\n context.logger.info('Local Backup initialized')\n }\n\n async shutdown(): Promise<void> {\n this.service = null\n }\n\n getService(): LocalBackupService {\n if (!this.service) throw new Error('Local Backup not initialized')\n return this.service\n }\n\n getCapabilityProvider<K extends keyof CapabilityProviderMap>(\n name: K,\n ): CapabilityProviderMap[K] | null {\n if (name === 'backup' as string && this.service) {\n return this.service as unknown as CapabilityProviderMap[K]\n }\n return null\n }\n\n getConfigSchema(): ConfigUISchema {\n return {\n sections: [\n {\n id: 'backup-retention',\n title: 'Backup Retention',\n description: 'How many local backup snapshots to keep on disk.',\n columns: 1,\n fields: [\n {\n type: 'number',\n key: 'retentionCount',\n label: 'Retention Count',\n description: 'Number of backup snapshots to keep before deleting the oldest',\n min: 1,\n max: 100,\n step: 1,\n },\n ],\n },\n ],\n }\n }\n\n getConfig(): Record<string, unknown> {\n return { ...this.currentConfig }\n }\n\n async onConfigChange(config: Record<string, unknown>): Promise<void> {\n this.currentConfig = {\n retentionCount: (config.retentionCount as number) ?? this.currentConfig.retentionCount,\n }\n }\n}\n"],"mappings":";AAAA,SAAS,kBAAkB;
|
|
1
|
+
{"version":3,"sources":["../src/builtins/local-backup/local-backup.ts","../src/builtins/local-backup/local-backup.addon.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\nimport type { IScopedLogger, IEventBus, IStorageLocation, BackupManifest } from '@camstack/types'\n\nexport type { BackupManifest }\n\nexport interface BackupConfig {\n readonly backupDir: string\n readonly retentionCount: number\n}\n\nexport class LocalBackupService {\n private manifests: BackupManifest[] = []\n\n constructor(\n private readonly config: BackupConfig,\n private readonly logger: IScopedLogger,\n private readonly eventBus: IEventBus,\n private readonly storage: IStorageLocation,\n ) {}\n\n /** Create a backup of specified locations */\n async backup(options?: {\n locations?: string[]\n label?: string\n }): Promise<BackupManifest> {\n const id = randomUUID()\n const timestamp = Date.now()\n const locations = options?.locations ?? ['config', 'events', 'logs']\n\n this.logger.info(`Starting backup ${id} (${locations.join(', ')})`)\n\n const manifest: BackupManifest = {\n id,\n timestamp,\n label: options?.label,\n locations,\n sizeMB: 0,\n path: `${this.config.backupDir}/${id}`,\n }\n\n const updated = [...this.manifests, manifest]\n this.manifests = updated\n\n await this.pruneOldBackups()\n\n this.eventBus.emit({\n id: randomUUID(),\n timestamp: new Date(),\n source: { type: 'addon', id: 'local-backup' },\n category: 'backup.completed',\n data: { backupId: id, locations: [...locations], sizeMB: manifest.sizeMB },\n })\n\n this.logger.info(`Backup ${id} completed`)\n return manifest\n }\n\n /** Restore from a backup */\n async restore(backupId: string): Promise<void> {\n const manifest = this.manifests.find((m) => m.id === backupId)\n if (!manifest) throw new Error(`Backup ${backupId} not found`)\n\n this.logger.info(`Restoring from backup ${backupId}`)\n\n this.eventBus.emit({\n id: randomUUID(),\n timestamp: new Date(),\n source: { type: 'addon', id: 'local-backup' },\n category: 'backup.restored',\n data: { backupId },\n })\n }\n\n /** List all backups sorted by timestamp descending */\n list(): readonly BackupManifest[] {\n return [...this.manifests].sort((a, b) => b.timestamp - a.timestamp)\n }\n\n /** Delete a specific backup */\n async delete(backupId: string): Promise<void> {\n this.manifests = this.manifests.filter((m) => m.id !== backupId)\n }\n\n private async pruneOldBackups(): Promise<void> {\n const sorted = [...this.manifests].sort((a, b) => a.timestamp - b.timestamp)\n\n while (sorted.length > this.config.retentionCount) {\n const oldest = sorted.shift()\n if (oldest) {\n this.logger.info(`Pruning old backup ${oldest.id}`)\n this.manifests = this.manifests.filter((m) => m.id !== oldest.id)\n }\n }\n }\n}\n","import * as path from 'node:path'\nimport type {\n ICamstackAddon, AddonManifest, AddonContext,\n IConfigurable, ConfigUISchema, CapabilityProviderMap,\n} from '@camstack/types'\nimport { LocalBackupService } from './local-backup'\nimport type { BackupConfig } from './local-backup'\n\nexport class LocalBackupAddon implements ICamstackAddon, IConfigurable {\n readonly manifest: AddonManifest = {\n id: 'local-backup',\n name: 'Local Backup',\n version: '1.0.0',\n capabilities: ['backup'],\n }\n\n private service: LocalBackupService | null = null\n private currentConfig = {\n retentionCount: 7,\n }\n\n async initialize(context: AddonContext): Promise<void> {\n this.currentConfig = {\n retentionCount: (context.addonConfig.retentionCount as number) ?? this.currentConfig.retentionCount,\n }\n const backupConfig: BackupConfig = {\n backupDir: path.join(context.locationPaths.data, 'backups'),\n retentionCount: this.currentConfig.retentionCount,\n }\n this.service = new LocalBackupService(\n backupConfig,\n context.logger,\n context.eventBus,\n context.storage,\n )\n context.logger.info('Local Backup initialized')\n }\n\n async shutdown(): Promise<void> {\n this.service = null\n }\n\n getService(): LocalBackupService {\n if (!this.service) throw new Error('Local Backup not initialized')\n return this.service\n }\n\n getCapabilityProvider<K extends keyof CapabilityProviderMap>(\n name: K,\n ): CapabilityProviderMap[K] | null {\n if (name === 'backup' as string && this.service) {\n return this.service as unknown as CapabilityProviderMap[K]\n }\n return null\n }\n\n getConfigSchema(): ConfigUISchema {\n return {\n sections: [\n {\n id: 'backup-retention',\n title: 'Backup Retention',\n description: 'How many local backup snapshots to keep on disk.',\n columns: 1,\n fields: [\n {\n type: 'number',\n key: 'retentionCount',\n label: 'Retention Count',\n description: 'Number of backup snapshots to keep before deleting the oldest',\n min: 1,\n max: 100,\n step: 1,\n },\n ],\n },\n ],\n }\n }\n\n getConfig(): Record<string, unknown> {\n return { ...this.currentConfig }\n }\n\n async onConfigChange(config: Record<string, unknown>): Promise<void> {\n this.currentConfig = {\n retentionCount: (config.retentionCount as number) ?? this.currentConfig.retentionCount,\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__esm,
|
|
3
|
+
__export
|
|
4
|
+
} from "./chunk-4JEXNFZZ.mjs";
|
|
5
|
+
|
|
6
|
+
// src/builtins/winston-logging/winston-destination.ts
|
|
7
|
+
var winston_destination_exports = {};
|
|
8
|
+
__export(winston_destination_exports, {
|
|
9
|
+
WinstonDestination: () => WinstonDestination
|
|
10
|
+
});
|
|
11
|
+
import * as winston from "winston";
|
|
12
|
+
import DailyRotateFile from "winston-daily-rotate-file";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
function formatScope(scope) {
|
|
15
|
+
if (scope.length === 0) return "";
|
|
16
|
+
const [first, ...rest] = scope;
|
|
17
|
+
const restFormatted = rest.map((s) => `[${s}]`).join("");
|
|
18
|
+
return `(${first})${restFormatted}`;
|
|
19
|
+
}
|
|
20
|
+
var WinstonDestination;
|
|
21
|
+
var init_winston_destination = __esm({
|
|
22
|
+
"src/builtins/winston-logging/winston-destination.ts"() {
|
|
23
|
+
"use strict";
|
|
24
|
+
WinstonDestination = class {
|
|
25
|
+
logger = null;
|
|
26
|
+
async initialize(config) {
|
|
27
|
+
const {
|
|
28
|
+
level = "info",
|
|
29
|
+
retentionDays = 30,
|
|
30
|
+
logsDir = path.join("camstack-data", "logs")
|
|
31
|
+
} = config ?? {};
|
|
32
|
+
const consoleFormat = winston.format.combine(
|
|
33
|
+
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
|
34
|
+
winston.format.colorize(),
|
|
35
|
+
winston.format.printf(({ timestamp, level: lvl, message, scope }) => {
|
|
36
|
+
const scopeStr = scope ? ` ${scope}` : "";
|
|
37
|
+
return `${timestamp} [${lvl}]${scopeStr} - ${message}`;
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
const fileFormat = winston.format.combine(
|
|
41
|
+
winston.format.timestamp(),
|
|
42
|
+
winston.format.json()
|
|
43
|
+
);
|
|
44
|
+
this.logger = winston.createLogger({
|
|
45
|
+
level,
|
|
46
|
+
transports: [
|
|
47
|
+
new winston.transports.Console({
|
|
48
|
+
format: consoleFormat
|
|
49
|
+
}),
|
|
50
|
+
new DailyRotateFile({
|
|
51
|
+
dirname: logsDir,
|
|
52
|
+
filename: "camstack-%DATE%.log",
|
|
53
|
+
datePattern: "YYYY-MM-DD",
|
|
54
|
+
maxFiles: `${retentionDays}d`,
|
|
55
|
+
format: fileFormat
|
|
56
|
+
})
|
|
57
|
+
]
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
write(entry) {
|
|
61
|
+
if (!this.logger) return;
|
|
62
|
+
const scope = formatScope(entry.scope);
|
|
63
|
+
const meta = entry.meta ?? {};
|
|
64
|
+
this.logger.log({
|
|
65
|
+
level: entry.level,
|
|
66
|
+
message: entry.message,
|
|
67
|
+
scope,
|
|
68
|
+
timestamp: entry.timestamp.toISOString(),
|
|
69
|
+
...meta
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async query(_filter) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
async shutdown() {
|
|
76
|
+
if (!this.logger) return;
|
|
77
|
+
await new Promise((resolve) => {
|
|
78
|
+
this.logger.on("finish", resolve);
|
|
79
|
+
this.logger.end();
|
|
80
|
+
});
|
|
81
|
+
this.logger = null;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// src/builtins/winston-logging/winston-logging.addon.ts
|
|
88
|
+
var winston_logging_addon_exports = {};
|
|
89
|
+
__export(winston_logging_addon_exports, {
|
|
90
|
+
WinstonLoggingAddon: () => WinstonLoggingAddon
|
|
91
|
+
});
|
|
92
|
+
var WinstonLoggingAddon;
|
|
93
|
+
var init_winston_logging_addon = __esm({
|
|
94
|
+
"src/builtins/winston-logging/winston-logging.addon.ts"() {
|
|
95
|
+
"use strict";
|
|
96
|
+
init_winston_destination();
|
|
97
|
+
WinstonLoggingAddon = class {
|
|
98
|
+
manifest = {
|
|
99
|
+
id: "winston-logging",
|
|
100
|
+
name: "Winston Logging",
|
|
101
|
+
version: "1.0.0",
|
|
102
|
+
capabilities: ["log-destination"]
|
|
103
|
+
};
|
|
104
|
+
destination = null;
|
|
105
|
+
currentConfig = {
|
|
106
|
+
level: "info",
|
|
107
|
+
retentionDays: 30
|
|
108
|
+
};
|
|
109
|
+
async initialize(context) {
|
|
110
|
+
this.currentConfig = {
|
|
111
|
+
level: context.addonConfig.level ?? this.currentConfig.level,
|
|
112
|
+
retentionDays: context.addonConfig.retentionDays ?? this.currentConfig.retentionDays
|
|
113
|
+
};
|
|
114
|
+
const logsDir = context.locationPaths.logs;
|
|
115
|
+
this.destination = new WinstonDestination();
|
|
116
|
+
await this.destination.initialize({ ...this.currentConfig, logsDir });
|
|
117
|
+
context.logger.info("Winston logging initialized");
|
|
118
|
+
}
|
|
119
|
+
async shutdown() {
|
|
120
|
+
await this.destination?.shutdown();
|
|
121
|
+
}
|
|
122
|
+
getDestination() {
|
|
123
|
+
if (!this.destination) throw new Error("Winston not initialized");
|
|
124
|
+
return this.destination;
|
|
125
|
+
}
|
|
126
|
+
getCapabilityProvider(name) {
|
|
127
|
+
if (name === "log-destination" && this.destination) {
|
|
128
|
+
return this.destination;
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
getConfigSchema() {
|
|
133
|
+
return {
|
|
134
|
+
sections: [
|
|
135
|
+
{
|
|
136
|
+
id: "winston-retention",
|
|
137
|
+
title: "Log Retention",
|
|
138
|
+
description: "How long Winston keeps rotated log files on disk.",
|
|
139
|
+
columns: 1,
|
|
140
|
+
fields: [
|
|
141
|
+
{
|
|
142
|
+
type: "number",
|
|
143
|
+
key: "retentionDays",
|
|
144
|
+
label: "Retention (days)",
|
|
145
|
+
description: "Number of days to keep rotated log files before deletion",
|
|
146
|
+
min: 1,
|
|
147
|
+
max: 365,
|
|
148
|
+
step: 1,
|
|
149
|
+
unit: "days"
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
getConfig() {
|
|
157
|
+
return { ...this.currentConfig };
|
|
158
|
+
}
|
|
159
|
+
async onConfigChange(config) {
|
|
160
|
+
this.currentConfig = {
|
|
161
|
+
level: config.level ?? this.currentConfig.level,
|
|
162
|
+
retentionDays: config.retentionDays ?? this.currentConfig.retentionDays
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
export {
|
|
170
|
+
WinstonDestination,
|
|
171
|
+
winston_destination_exports,
|
|
172
|
+
init_winston_destination,
|
|
173
|
+
WinstonLoggingAddon,
|
|
174
|
+
winston_logging_addon_exports,
|
|
175
|
+
init_winston_logging_addon
|
|
176
|
+
};
|
|
177
|
+
//# sourceMappingURL=chunk-XSLBW5C2.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/builtins/winston-logging/winston-destination.ts","../src/builtins/winston-logging/winston-logging.addon.ts"],"sourcesContent":["import * as winston from 'winston'\nimport DailyRotateFile from 'winston-daily-rotate-file'\nimport * as path from 'node:path'\nimport type { ILogDestination, LogEntry, LogFilter } from '@camstack/types'\n\ninterface WinstonConfig {\n readonly level: string\n readonly retentionDays: number\n /** Resolved absolute path to the logs directory (replaces the old dataPath field). */\n readonly logsDir: string\n}\n\nfunction formatScope(scope: readonly string[]): string {\n if (scope.length === 0) return ''\n const [first, ...rest] = scope\n const restFormatted = rest.map((s) => `[${s}]`).join('')\n return `(${first})${restFormatted}`\n}\n\nexport class WinstonDestination implements ILogDestination {\n private logger: winston.Logger | null = null\n\n async initialize(config?: WinstonConfig): Promise<void> {\n const {\n level = 'info',\n retentionDays = 30,\n logsDir = path.join('camstack-data', 'logs'),\n } = config ?? {}\n\n const consoleFormat = winston.format.combine(\n winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),\n winston.format.colorize(),\n winston.format.printf(({ timestamp, level: lvl, message, scope }) => {\n const scopeStr = scope ? ` ${scope}` : ''\n return `${timestamp} [${lvl}]${scopeStr} - ${message}`\n }),\n )\n\n const fileFormat = winston.format.combine(\n winston.format.timestamp(),\n winston.format.json(),\n )\n\n this.logger = winston.createLogger({\n level,\n transports: [\n new winston.transports.Console({\n format: consoleFormat,\n }),\n new DailyRotateFile({\n dirname: logsDir,\n filename: 'camstack-%DATE%.log',\n datePattern: 'YYYY-MM-DD',\n maxFiles: `${retentionDays}d`,\n format: fileFormat,\n }),\n ],\n })\n }\n\n write(entry: LogEntry): void {\n if (!this.logger) return\n\n const scope = formatScope(entry.scope)\n const meta = entry.meta ?? {}\n\n this.logger.log({\n level: entry.level,\n message: entry.message,\n scope,\n timestamp: entry.timestamp.toISOString(),\n ...meta,\n })\n }\n\n async query(_filter: LogFilter): Promise<readonly LogEntry[]> {\n // File-based log querying is not implemented; use structured storage for log queries\n return []\n }\n\n async shutdown(): Promise<void> {\n if (!this.logger) return\n\n await new Promise<void>((resolve) => {\n this.logger!.on('finish', resolve)\n this.logger!.end()\n })\n this.logger = null\n }\n}\n","import type {\n ICamstackAddon,\n AddonManifest,\n AddonContext,\n IConfigurable,\n ConfigUISchema,\n CapabilityProviderMap,\n} from '@camstack/types'\nimport { WinstonDestination } from './winston-destination'\n\nexport class WinstonLoggingAddon implements ICamstackAddon, IConfigurable {\n readonly manifest: AddonManifest = {\n id: 'winston-logging',\n name: 'Winston Logging',\n version: '1.0.0',\n capabilities: ['log-destination'],\n }\n\n private destination: WinstonDestination | null = null\n private currentConfig = {\n level: 'info',\n retentionDays: 30,\n }\n\n async initialize(context: AddonContext): Promise<void> {\n this.currentConfig = {\n level: (context.addonConfig.level as string) ?? this.currentConfig.level,\n retentionDays: (context.addonConfig.retentionDays as number) ?? this.currentConfig.retentionDays,\n }\n const logsDir = context.locationPaths.logs\n this.destination = new WinstonDestination()\n await this.destination.initialize({ ...this.currentConfig, logsDir })\n context.logger.info('Winston logging initialized')\n }\n\n async shutdown(): Promise<void> {\n await this.destination?.shutdown()\n }\n\n getDestination(): WinstonDestination {\n if (!this.destination) throw new Error('Winston not initialized')\n return this.destination\n }\n\n getCapabilityProvider<K extends keyof CapabilityProviderMap>(\n name: K,\n ): CapabilityProviderMap[K] | null {\n if (name === 'log-destination' && this.destination) {\n return this.destination as unknown as CapabilityProviderMap[K]\n }\n return null\n }\n\n getConfigSchema(): ConfigUISchema {\n return {\n sections: [\n {\n id: 'winston-retention',\n title: 'Log Retention',\n description: 'How long Winston keeps rotated log files on disk.',\n columns: 1,\n fields: [\n {\n type: 'number',\n key: 'retentionDays',\n label: 'Retention (days)',\n description: 'Number of days to keep rotated log files before deletion',\n min: 1,\n max: 365,\n step: 1,\n unit: 'days',\n },\n ],\n },\n ],\n }\n }\n\n getConfig(): Record<string, unknown> {\n return { ...this.currentConfig }\n }\n\n async onConfigChange(config: Record<string, unknown>): Promise<void> {\n this.currentConfig = {\n level: (config.level as string) ?? this.currentConfig.level,\n retentionDays: (config.retentionDays as number) ?? this.currentConfig.retentionDays,\n }\n }\n}\n"],"mappings":";AAAA,YAAY,aAAa;AACzB,OAAO,qBAAqB;AAC5B,YAAY,UAAU;AAUtB,SAAS,YAAY,OAAkC;AACrD,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,QAAM,CAAC,OAAO,GAAG,IAAI,IAAI;AACzB,QAAM,gBAAgB,KAAK,IAAI,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE;AACvD,SAAO,IAAI,KAAK,IAAI,aAAa;AACnC;
|
|
1
|
+
{"version":3,"sources":["../src/builtins/winston-logging/winston-destination.ts","../src/builtins/winston-logging/winston-logging.addon.ts"],"sourcesContent":["import * as winston from 'winston'\nimport DailyRotateFile from 'winston-daily-rotate-file'\nimport * as path from 'node:path'\nimport type { ILogDestination, LogEntry, LogFilter } from '@camstack/types'\n\ninterface WinstonConfig {\n readonly level: string\n readonly retentionDays: number\n /** Resolved absolute path to the logs directory (replaces the old dataPath field). */\n readonly logsDir: string\n}\n\nfunction formatScope(scope: readonly string[]): string {\n if (scope.length === 0) return ''\n const [first, ...rest] = scope\n const restFormatted = rest.map((s) => `[${s}]`).join('')\n return `(${first})${restFormatted}`\n}\n\nexport class WinstonDestination implements ILogDestination {\n private logger: winston.Logger | null = null\n\n async initialize(config?: WinstonConfig): Promise<void> {\n const {\n level = 'info',\n retentionDays = 30,\n logsDir = path.join('camstack-data', 'logs'),\n } = config ?? {}\n\n const consoleFormat = winston.format.combine(\n winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),\n winston.format.colorize(),\n winston.format.printf(({ timestamp, level: lvl, message, scope }) => {\n const scopeStr = scope ? ` ${scope}` : ''\n return `${timestamp} [${lvl}]${scopeStr} - ${message}`\n }),\n )\n\n const fileFormat = winston.format.combine(\n winston.format.timestamp(),\n winston.format.json(),\n )\n\n this.logger = winston.createLogger({\n level,\n transports: [\n new winston.transports.Console({\n format: consoleFormat,\n }),\n new DailyRotateFile({\n dirname: logsDir,\n filename: 'camstack-%DATE%.log',\n datePattern: 'YYYY-MM-DD',\n maxFiles: `${retentionDays}d`,\n format: fileFormat,\n }),\n ],\n })\n }\n\n write(entry: LogEntry): void {\n if (!this.logger) return\n\n const scope = formatScope(entry.scope)\n const meta = entry.meta ?? {}\n\n this.logger.log({\n level: entry.level,\n message: entry.message,\n scope,\n timestamp: entry.timestamp.toISOString(),\n ...meta,\n })\n }\n\n async query(_filter: LogFilter): Promise<readonly LogEntry[]> {\n // File-based log querying is not implemented; use structured storage for log queries\n return []\n }\n\n async shutdown(): Promise<void> {\n if (!this.logger) return\n\n await new Promise<void>((resolve) => {\n this.logger!.on('finish', resolve)\n this.logger!.end()\n })\n this.logger = null\n }\n}\n","import type {\n ICamstackAddon,\n AddonManifest,\n AddonContext,\n IConfigurable,\n ConfigUISchema,\n CapabilityProviderMap,\n} from '@camstack/types'\nimport { WinstonDestination } from './winston-destination'\n\nexport class WinstonLoggingAddon implements ICamstackAddon, IConfigurable {\n readonly manifest: AddonManifest = {\n id: 'winston-logging',\n name: 'Winston Logging',\n version: '1.0.0',\n capabilities: ['log-destination'],\n }\n\n private destination: WinstonDestination | null = null\n private currentConfig = {\n level: 'info',\n retentionDays: 30,\n }\n\n async initialize(context: AddonContext): Promise<void> {\n this.currentConfig = {\n level: (context.addonConfig.level as string) ?? this.currentConfig.level,\n retentionDays: (context.addonConfig.retentionDays as number) ?? this.currentConfig.retentionDays,\n }\n const logsDir = context.locationPaths.logs\n this.destination = new WinstonDestination()\n await this.destination.initialize({ ...this.currentConfig, logsDir })\n context.logger.info('Winston logging initialized')\n }\n\n async shutdown(): Promise<void> {\n await this.destination?.shutdown()\n }\n\n getDestination(): WinstonDestination {\n if (!this.destination) throw new Error('Winston not initialized')\n return this.destination\n }\n\n getCapabilityProvider<K extends keyof CapabilityProviderMap>(\n name: K,\n ): CapabilityProviderMap[K] | null {\n if (name === 'log-destination' && this.destination) {\n return this.destination as unknown as CapabilityProviderMap[K]\n }\n return null\n }\n\n getConfigSchema(): ConfigUISchema {\n return {\n sections: [\n {\n id: 'winston-retention',\n title: 'Log Retention',\n description: 'How long Winston keeps rotated log files on disk.',\n columns: 1,\n fields: [\n {\n type: 'number',\n key: 'retentionDays',\n label: 'Retention (days)',\n description: 'Number of days to keep rotated log files before deletion',\n min: 1,\n max: 365,\n step: 1,\n unit: 'days',\n },\n ],\n },\n ],\n }\n }\n\n getConfig(): Record<string, unknown> {\n return { ...this.currentConfig }\n }\n\n async onConfigChange(config: Record<string, unknown>): Promise<void> {\n this.currentConfig = {\n level: (config.level as string) ?? this.currentConfig.level,\n retentionDays: (config.retentionDays as number) ?? this.currentConfig.retentionDays,\n }\n }\n}\n"],"mappings":"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","names":[]}
|