@starkeep/storage-sqlite 0.1.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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapter.ts","../src/serialization.ts","../src/query-builder.ts","../src/schema/bootstrap.ts","../src/control-plane-stores.ts","../src/app-syncable/namespace.ts","../src/app-syncable/apply.ts"],"sourcesContent":["import { DatabaseSync } from \"node:sqlite\";\nimport { mkdirSync, existsSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport type { DataRecord, HLCTimestamp, MetadataRow, StarkeepId } from \"@starkeep/protocol-primitives\";\nimport { serializeHLC, sqliteMetadataTableName } from \"@starkeep/protocol-primitives\";\nimport type {\n DatabaseAdapter,\n Query,\n QueryResult,\n BatchOperation,\n Transaction,\n} from \"@starkeep/storage-adapter\";\nimport { StorageError, TransactionError } from \"@starkeep/storage-adapter\";\nimport { recordToRow, rowToRecord, type SqliteRow } from \"./serialization.js\";\nimport { buildSelectQuery } from \"./query-builder.js\";\nimport { initializeLocalSchema } from \"./schema/bootstrap.js\";\n\nexport interface SqliteDatabaseAdapterOptions {\n path: string | \":memory:\";\n}\n\nexport class SqliteDatabaseAdapter implements DatabaseAdapter {\n private database: DatabaseSync | null = null;\n private readonly options: SqliteDatabaseAdapterOptions;\n\n constructor(options: SqliteDatabaseAdapterOptions) {\n this.options = options;\n }\n\n async init(): Promise<void> {\n if (this.database) return;\n if (this.options.path !== \":memory:\") {\n const dir = dirname(this.options.path);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n }\n this.database = new DatabaseSync(this.options.path);\n initializeLocalSchema(this.database);\n }\n\n async close(): Promise<void> {\n this.database?.close();\n this.database = null;\n }\n\n async healthCheck(): Promise<boolean> {\n if (!this.database) return false;\n try {\n this.database.prepare(\"SELECT 1\").get();\n return true;\n } catch {\n return false;\n }\n }\n\n private getDatabase(): DatabaseSync {\n if (!this.database) throw new StorageError(\"Database not initialized. Call init() first.\");\n return this.database;\n }\n\n /**\n * Returns the raw SQLite connection so sibling subsystems (e.g. the sync\n * engine's change log + state store) can create side tables in the same\n * database file. Callers must only use this after `init()`.\n */\n getRawDatabase(): DatabaseSync {\n return this.getDatabase();\n }\n\n private runStmt(sql: string, ...params: unknown[]): void {\n this.getDatabase().prepare(sql).run(...(params as Parameters<ReturnType<DatabaseSync[\"prepare\"]>[\"run\"]>));\n }\n\n private getRow<T = SqliteRow>(sql: string, ...params: unknown[]): T | undefined {\n return this.getDatabase().prepare(sql).get(\n ...(params as Parameters<ReturnType<DatabaseSync[\"prepare\"]>[\"get\"]>),\n ) as unknown as T | undefined;\n }\n\n private allRows<T = SqliteRow>(sql: string, ...params: unknown[]): T[] {\n return this.getDatabase().prepare(sql).all(\n ...(params as Parameters<ReturnType<DatabaseSync[\"prepare\"]>[\"all\"]>),\n ) as unknown as T[];\n }\n\n async put(record: DataRecord): Promise<void> {\n const row = recordToRow(record);\n const columns = Object.keys(row);\n const placeholders = columns.map(() => \"?\").join(\", \");\n const updates = columns\n .filter((column) => column !== \"id\")\n .map((column) => `${column} = excluded.${column}`)\n .join(\", \");\n const sql = `INSERT INTO shared_records (${columns.join(\", \")}) VALUES (${placeholders}) ON CONFLICT(id) DO UPDATE SET ${updates}`;\n this.runStmt(sql, ...Object.values(row));\n }\n\n async get(id: StarkeepId): Promise<DataRecord | null> {\n const row = this.getRow<SqliteRow>(\"SELECT * FROM shared_records WHERE id = ?\", id);\n return row ? rowToRecord(row) : null;\n }\n\n async delete(id: StarkeepId, hlc: HLCTimestamp): Promise<void> {\n const ts = serializeHLC(hlc);\n this.runStmt(\n \"UPDATE shared_records SET deleted_at = ?, updated_at = ? WHERE id = ?\",\n ts,\n ts,\n id,\n );\n }\n\n async query(query: Query): Promise<QueryResult> {\n const { sql, params } = buildSelectQuery(query);\n const rows = this.allRows<SqliteRow>(sql, ...params);\n\n const limit = query.limit;\n const hasMore = limit ? rows.length > limit : false;\n const resultRows = hasMore ? rows.slice(0, limit) : rows;\n\n return {\n records: resultRows.map(rowToRecord),\n nextCursor: hasMore ? resultRows[resultRows.length - 1].id : null,\n hasMore,\n };\n }\n\n async batch(operations: BatchOperation[]): Promise<void> {\n this.getDatabase().exec(\"BEGIN\");\n try {\n for (const operation of operations) {\n if (operation.type === \"put\") {\n await this.put(operation.record);\n } else {\n await this.delete(operation.id, operation.hlc);\n }\n }\n this.getDatabase().exec(\"COMMIT\");\n } catch (error) {\n this.getDatabase().exec(\"ROLLBACK\");\n throw error;\n }\n }\n\n async transaction<T>(callback: (transaction: Transaction) => Promise<T>): Promise<T> {\n this.getDatabase().exec(\"SAVEPOINT starkeep_tx\");\n try {\n const transaction: Transaction = {\n put: async (record) => this.put(record),\n get: async (id) => this.get(id),\n delete: async (id, hlc) => this.delete(id, hlc),\n query: async (query) => this.query(query),\n };\n const result = await callback(transaction);\n this.getDatabase().exec(\"RELEASE SAVEPOINT starkeep_tx\");\n return result;\n } catch (error) {\n this.getDatabase().exec(\"ROLLBACK TO SAVEPOINT starkeep_tx\");\n this.getDatabase().exec(\"RELEASE SAVEPOINT starkeep_tx\");\n throw new TransactionError(\"Transaction failed\", error);\n }\n }\n\n async putMetadata(typeId: string, row: MetadataRow): Promise<void> {\n const table = sqliteMetadataTableName(typeId);\n const cols: string[] = [\"record_id\"];\n const values: unknown[] = [row.recordId];\n for (const [key, value] of Object.entries(row)) {\n if (key === \"recordId\") continue;\n cols.push(key);\n values.push(value);\n }\n const placeholders = cols.map(() => \"?\").join(\", \");\n const updates = cols\n .filter((c) => c !== \"record_id\")\n .map((c) => `${c} = excluded.${c}`)\n .join(\", \");\n const sql = updates\n ? `INSERT INTO ${table} (${cols.join(\", \")}) VALUES (${placeholders}) ON CONFLICT(record_id) DO UPDATE SET ${updates}`\n : `INSERT INTO ${table} (${cols.join(\", \")}) VALUES (${placeholders}) ON CONFLICT(record_id) DO NOTHING`;\n this.runStmt(sql, ...values);\n }\n\n async getMetadata(typeId: string, recordId: StarkeepId): Promise<MetadataRow | null> {\n const table = sqliteMetadataTableName(typeId);\n const row = this.getRow<Record<string, unknown>>(\n `SELECT * FROM ${table} WHERE record_id = ?`,\n recordId,\n );\n if (!row) return null;\n return columnsToMetadataRow(recordId, row);\n }\n\n async getMetadataByIds(\n typeId: string,\n recordIds: StarkeepId[],\n ): Promise<Map<StarkeepId, MetadataRow>> {\n const result = new Map<StarkeepId, MetadataRow>();\n if (recordIds.length === 0) return result;\n const table = sqliteMetadataTableName(typeId);\n const placeholders = recordIds.map(() => \"?\").join(\", \");\n const rows = this.allRows<Record<string, unknown>>(\n `SELECT * FROM ${table} WHERE record_id IN (${placeholders})`,\n ...recordIds,\n );\n for (const row of rows) {\n const recordId = row[\"record_id\"] as StarkeepId;\n result.set(recordId, columnsToMetadataRow(recordId, row));\n }\n return result;\n }\n\n async deleteMetadata(typeId: string, recordId: StarkeepId): Promise<void> {\n const table = sqliteMetadataTableName(typeId);\n this.runStmt(`DELETE FROM ${table} WHERE record_id = ?`, recordId);\n }\n\n}\n\nfunction columnsToMetadataRow(\n recordId: StarkeepId,\n columns: Record<string, unknown>,\n): MetadataRow {\n const row: MetadataRow = { recordId };\n for (const [key, value] of Object.entries(columns)) {\n if (key === \"record_id\") continue;\n row[key] = value;\n }\n return row;\n}\n","import type { DataRecord, MetadataRow, StarkeepId } from \"@starkeep/protocol-primitives\";\nimport { serializeHLC, deserializeHLC, createStarkeepId } from \"@starkeep/protocol-primitives\";\n\nexport interface SqliteRow {\n id: string;\n type: string;\n created_at: string;\n updated_at: string;\n owner_id: string;\n deleted_at: string | null;\n version: number;\n content_hash: string;\n object_storage_key: string;\n mime_type: string;\n size_bytes: number;\n original_filename: string | null;\n origin_app_id: string;\n parent_id: string | null;\n}\n\nexport function recordToRow(record: DataRecord): SqliteRow {\n return {\n id: record.id,\n type: record.type,\n created_at: serializeHLC(record.createdAt),\n updated_at: serializeHLC(record.updatedAt),\n owner_id: record.ownerId,\n deleted_at: record.deletedAt ? serializeHLC(record.deletedAt) : null,\n version: record.version,\n content_hash: record.contentHash,\n object_storage_key: record.objectStorageKey,\n mime_type: record.mimeType,\n size_bytes: record.sizeBytes,\n original_filename: record.originalFilename,\n origin_app_id: record.originAppId,\n parent_id: record.parentId,\n };\n}\n\nexport function rowToRecord(row: SqliteRow): DataRecord {\n return {\n id: createStarkeepId(row.id),\n kind: \"data\",\n type: row.type,\n createdAt: deserializeHLC(row.created_at),\n updatedAt: deserializeHLC(row.updated_at),\n ownerId: row.owner_id,\n deletedAt: row.deleted_at ? deserializeHLC(row.deleted_at) : null,\n version: row.version,\n contentHash: row.content_hash,\n objectStorageKey: row.object_storage_key,\n mimeType: row.mime_type,\n sizeBytes: row.size_bytes,\n originalFilename: row.original_filename,\n originAppId: row.origin_app_id,\n parentId: row.parent_id ? createStarkeepId(row.parent_id) : null,\n };\n}\n\n/**\n * Convert SQLite column-keyed row data into a MetadataRow keyed by recordId\n * plus camelCase or snake_case columns. We pass columns through as-is from\n * the DB so callers can address them however they prefer.\n */\nexport function metadataRowFromColumns(\n recordId: StarkeepId,\n columns: Record<string, unknown>,\n): MetadataRow {\n const row: MetadataRow = { recordId };\n for (const [key, value] of Object.entries(columns)) {\n if (key === \"record_id\") continue;\n row[key] = value;\n }\n return row;\n}\n","import type { Query } from \"@starkeep/storage-adapter\";\nimport {\n DummyDriver,\n Kysely,\n SqliteAdapter,\n SqliteIntrospector,\n SqliteQueryCompiler,\n type ExpressionBuilder,\n type SelectQueryBuilder,\n} from \"kysely\";\n\n// External (camelCase) → column name (snake_case). Unknown fields pass through.\nconst FIELD_MAP: Record<string, string> = {\n id: \"id\",\n type: \"type\",\n createdAt: \"created_at\",\n updatedAt: \"updated_at\",\n ownerId: \"owner_id\",\n deletedAt: \"deleted_at\",\n version: \"version\",\n contentHash: \"content_hash\",\n objectStorageKey: \"object_storage_key\",\n mimeType: \"mime_type\",\n sizeBytes: \"size_bytes\",\n originAppId: \"origin_app_id\",\n parentId: \"parent_id\",\n originalFilename: \"original_filename\",\n};\n\nfunction mapField(field: string): string {\n return FIELD_MAP[field] ?? field;\n}\n\n// Single dynamic-schema Kysely instance used only to compile SQL — never\n// executes. The DummyDriver lets us reuse Kysely's compiler without pulling\n// in a real connection. `any` keeps it dialect-agnostic at the row level;\n// column names are validated against the live SQLite schema at runtime.\ntype DB = Record<string, Record<string, unknown>>;\nconst compiler = new Kysely<DB>({\n dialect: {\n createAdapter: () => new SqliteAdapter(),\n createDriver: () => new DummyDriver(),\n createIntrospector: (db) => new SqliteIntrospector(db),\n createQueryCompiler: () => new SqliteQueryCompiler(),\n },\n});\n\nexport interface BuiltQuery {\n sql: string;\n params: unknown[];\n}\n\nexport function buildSelectQuery(query: Query): BuiltQuery {\n type Qb = SelectQueryBuilder<DB, \"shared_records\", unknown>;\n let qb = compiler.selectFrom(\"shared_records\").selectAll() as Qb;\n\n if (query.type) {\n qb = qb.where(\"type\", \"=\", query.type);\n }\n\n if (query.filters) {\n for (const filter of query.filters) {\n qb = applyFilter(qb, filter);\n }\n }\n\n if (query.cursor) {\n qb = qb.where(\"id\", \">\", query.cursor);\n }\n\n if (query.sort && query.sort.length > 0) {\n for (const s of query.sort) {\n qb = qb.orderBy(mapField(s.field), s.direction === \"desc\" ? \"desc\" : \"asc\");\n }\n } else {\n qb = qb.orderBy(\"id\", \"asc\");\n }\n\n if (query.limit) {\n qb = qb.limit(query.limit + 1); // +1 to detect hasMore\n }\n\n const compiled = qb.compile();\n return { sql: compiled.sql, params: [...compiled.parameters] };\n}\n\nfunction applyFilter<Qb extends SelectQueryBuilder<DB, \"shared_records\", unknown>>(\n qb: Qb,\n filter: { field: string; operator: string; value?: unknown },\n): Qb {\n const col = mapField(filter.field);\n switch (filter.operator) {\n case \"eq\": return qb.where(col, \"=\", filter.value) as Qb;\n case \"neq\": return qb.where(col, \"!=\", filter.value) as Qb;\n case \"gt\": return qb.where(col, \">\", filter.value) as Qb;\n case \"gte\": return qb.where(col, \">=\", filter.value) as Qb;\n case \"lt\": return qb.where(col, \"<\", filter.value) as Qb;\n case \"lte\": return qb.where(col, \"<=\", filter.value) as Qb;\n case \"in\": return qb.where(col, \"in\", filter.value as unknown[]) as Qb;\n case \"like\": return qb.where(col, \"like\", `%${filter.value}%`) as Qb;\n case \"isNull\":\n return qb.where((eb: ExpressionBuilder<DB, \"shared_records\">) => eb(col, \"is\", null)) as Qb;\n case \"isNotNull\":\n return qb.where((eb: ExpressionBuilder<DB, \"shared_records\">) => eb(col, \"is not\", null)) as Qb;\n default: return qb;\n }\n}\n","import type { DatabaseSync } from \"node:sqlite\";\nimport { CATEGORIES, sqliteMetadataDdl } from \"@starkeep/protocol-primitives\";\n\n/**\n * Local sqlite schema bootstrap.\n *\n * Layout follows desired-state-roles-and-permissions.md \"DSQL DDL ... Local\n * SQLite asymmetry\" section: prefix-named tables in one file instead of\n * separate schemas. Only tables that something in phase 1 actually reads or\n * writes are created here — promotion, metadata enrichment, and the janitor\n * land as separate workstreams with their own DDL.\n *\n * - shared_records — all shared data, all types (file-backed only)\n * - shared_record_<category>_metadata — per-category metadata rows (typed columns)\n * - shared_access_grants — per-app, per-extension permissions\n * - shared_app_registry — installed apps + HMAC secrets\n * - shared_app_install_steps — idempotent install/uninstall ledger\n * - access_policies — control-plane: AccessControlEngine policies\n *\n * `sharing_tokens` is not persisted anywhere today — local uses the disabled\n * stub store and no cloud-side table or endpoint exists. The redemption path\n * is left for a future workstream.\n *\n * No migration system: this is a fresh-start schema. The user removes\n * ~/.starkeep/data.db (or the local-data-server's STARKEEP_DIR is fresh)\n * before this code runs.\n */\nexport function initializeLocalSchema(db: DatabaseSync): void {\n db.exec(\"PRAGMA journal_mode = WAL\");\n db.exec(\"PRAGMA foreign_keys = ON\");\n\n db.exec(`\n CREATE TABLE IF NOT EXISTS shared_records (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n owner_id TEXT NOT NULL,\n deleted_at TEXT,\n version INTEGER NOT NULL DEFAULT 1,\n content_hash TEXT NOT NULL,\n object_storage_key TEXT NOT NULL,\n mime_type TEXT NOT NULL,\n size_bytes INTEGER NOT NULL,\n original_filename TEXT,\n origin_app_id TEXT NOT NULL,\n parent_id TEXT\n )\n `);\n db.exec(\"CREATE INDEX IF NOT EXISTS idx_shared_records_type ON shared_records(type)\");\n db.exec(\"CREATE INDEX IF NOT EXISTS idx_shared_records_origin_app ON shared_records(origin_app_id)\");\n db.exec(\"CREATE INDEX IF NOT EXISTS idx_shared_records_parent_id ON shared_records(parent_id)\");\n // Duplicate-file prevention: (filename + bytes) is unique per owner among\n // live records. Tombstoned rows (deleted_at IS NOT NULL) are excluded so a\n // re-upload after delete is allowed. Records with NULL filename are not\n // constrained — the rule requires both filename and content to match.\n db.exec(\n \"CREATE UNIQUE INDEX IF NOT EXISTS uq_shared_records_owner_filename_hash \" +\n \"ON shared_records(owner_id, original_filename, content_hash) \" +\n \"WHERE deleted_at IS NULL AND original_filename IS NOT NULL\",\n );\n\n db.exec(`\n CREATE TABLE IF NOT EXISTS shared_access_grants (\n app_id TEXT NOT NULL,\n type_id TEXT NOT NULL,\n access TEXT NOT NULL CHECK (access IN ('read', 'readwrite')),\n metadata_write INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n PRIMARY KEY (app_id, type_id)\n )\n `);\n db.exec(\"CREATE INDEX IF NOT EXISTS idx_shared_access_grants_app ON shared_access_grants(app_id)\");\n\n db.exec(`\n CREATE TABLE IF NOT EXISTS shared_app_registry (\n app_id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n version TEXT NOT NULL,\n tier TEXT NOT NULL DEFAULT 'app',\n manifest TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'installing',\n hmac_secret TEXT NOT NULL,\n installed_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n )\n `);\n\n db.exec(`\n CREATE TABLE IF NOT EXISTS shared_app_install_steps (\n app_id TEXT NOT NULL,\n operation TEXT NOT NULL CHECK (operation IN ('install', 'uninstall')),\n step TEXT NOT NULL,\n status TEXT NOT NULL CHECK (status IN ('pending', 'done', 'failed')),\n error TEXT,\n updated_at TEXT NOT NULL DEFAULT (datetime('now')),\n PRIMARY KEY (app_id, operation, step)\n )\n `);\n\n // App-specific syncable namespace registry. One row per installed app that\n // declared `infraRequirements.appSpecificSyncable`. Lists the tables the\n // installer materialized as `<app_id>_syncable_<name>` and whether the app\n // opted into the `apps/<app_id>/syncable/` file prefix. Read by the SDK to\n // gate row CRUD/file ops and by the sync engine to enumerate what to sync.\n db.exec(`\n CREATE TABLE IF NOT EXISTS app_syncable_namespaces (\n app_id TEXT PRIMARY KEY,\n tables_json TEXT NOT NULL,\n files_enabled INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n )\n `);\n\n // Control-plane: access policies issued via sdk.accessControl.createPolicy.\n // Instance-local; never synced. See AccessControlEngine.\n db.exec(`\n CREATE TABLE IF NOT EXISTS access_policies (\n policy_id TEXT PRIMARY KEY,\n subject_type TEXT NOT NULL,\n subject_id TEXT NOT NULL,\n resource_type TEXT NOT NULL,\n resource_id TEXT NOT NULL,\n permissions TEXT NOT NULL,\n granted_at TEXT NOT NULL,\n expires_at TEXT\n )\n `);\n db.exec(\"CREATE INDEX IF NOT EXISTS idx_access_policies_subject ON access_policies(subject_type, subject_id)\");\n\n // Per-category metadata tables on parity with DSQL. Generated from CATEGORIES\n // so adding a category or a column is a single edit in @starkeep/protocol-primitives's\n // core-types.ts. `other` has no metadata columns and gets no table.\n for (const c of CATEGORIES) {\n if (c.id === \"other\") continue;\n db.exec(sqliteMetadataDdl(c));\n }\n}\n","import type { DatabaseSync } from \"node:sqlite\";\nimport {\n createStarkeepId,\n deserializeHLC,\n serializeHLC,\n type StarkeepId,\n} from \"@starkeep/protocol-primitives\";\nimport type {\n AccessPolicy,\n AccessPolicyStore,\n Permission,\n ResourceType,\n SubjectType,\n} from \"@starkeep/access-control\";\n\n/**\n * SQLite-backed AccessPolicyStore. Schema is bootstrapped in\n * storage-sqlite/src/schema/bootstrap.ts. Permissions are stored as a\n * comma-separated string — never user-supplied, always picked from the\n * Permission enum so no escaping is required.\n */\nexport function createSqliteAccessPolicyStore(db: DatabaseSync): AccessPolicyStore {\n return {\n async putPolicy(policy: AccessPolicy): Promise<void> {\n db.prepare(\n `INSERT INTO access_policies (\n policy_id, subject_type, subject_id, resource_type, resource_id,\n permissions, granted_at, expires_at\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(policy_id) DO UPDATE SET\n subject_type = excluded.subject_type,\n subject_id = excluded.subject_id,\n resource_type = excluded.resource_type,\n resource_id = excluded.resource_id,\n permissions = excluded.permissions,\n granted_at = excluded.granted_at,\n expires_at = excluded.expires_at`,\n ).run(\n policy.policyId,\n policy.subjectType,\n policy.subjectId,\n policy.resourceType,\n policy.resourceId,\n policy.permissions.join(\",\"),\n serializeHLC(policy.grantedAt),\n policy.expiresAt ? serializeHLC(policy.expiresAt) : null,\n );\n },\n\n async getPolicy(policyId: StarkeepId): Promise<AccessPolicy | null> {\n const row = db\n .prepare(\"SELECT * FROM access_policies WHERE policy_id = ?\")\n .get(policyId) as PolicyRow | undefined;\n return row ? rowToPolicy(row) : null;\n },\n\n async listPolicies(): Promise<AccessPolicy[]> {\n const rows = db.prepare(\"SELECT * FROM access_policies\").all() as unknown as PolicyRow[];\n return rows.map(rowToPolicy);\n },\n\n async deletePolicy(policyId: StarkeepId): Promise<void> {\n db.prepare(\"DELETE FROM access_policies WHERE policy_id = ?\").run(policyId);\n },\n };\n}\n\ninterface PolicyRow {\n policy_id: string;\n subject_type: string;\n subject_id: string;\n resource_type: string;\n resource_id: string;\n permissions: string;\n granted_at: string;\n expires_at: string | null;\n}\n\nfunction rowToPolicy(row: PolicyRow): AccessPolicy {\n return {\n policyId: createStarkeepId(row.policy_id),\n subjectType: row.subject_type as SubjectType,\n subjectId: row.subject_id,\n resourceType: row.resource_type as ResourceType,\n resourceId: row.resource_id,\n permissions: row.permissions.split(\",\").filter(Boolean) as Permission[],\n grantedAt: deserializeHLC(row.granted_at),\n expiresAt: row.expires_at ? deserializeHLC(row.expires_at) : null,\n };\n}\n\n","import type { DatabaseSync } from \"node:sqlite\";\nimport type {\n AppSyncableNamespace,\n AppSyncableTableInfo,\n AppSyncableNamespaceStore,\n} from \"@starkeep/shared-space-api\";\n\nexport type { AppSyncableNamespace, AppSyncableTableInfo };\n\n// `<appId>_syncable_<table>` is what the manifest convention pins, but appIds\n// may contain dashes (e.g. \"cloud-data-server\") which aren't legal SQLite\n// identifiers. Normalize the same way as cloud-side PG roles.\nfunction normalizeAppId(appId: string): string {\n return appId.toLowerCase().replace(/-/g, \"_\");\n}\n\nexport function appSyncableTableName(appId: string, tableName: string): string {\n return `${normalizeAppId(appId)}_syncable_${tableName}`;\n}\n\nfunction rowToNamespace(r: {\n app_id: string;\n tables_json: string;\n files_enabled: number;\n}): AppSyncableNamespace {\n const tables: AppSyncableTableInfo[] = JSON.parse(r.tables_json);\n return {\n appId: r.app_id,\n tables,\n filesEnabled: r.files_enabled === 1,\n tableNames: tables.map((t) => t.name),\n };\n}\n\nexport function upsertAppSyncableNamespace(\n db: DatabaseSync,\n appId: string,\n tables: AppSyncableTableInfo[],\n filesEnabled: boolean,\n): void {\n db.prepare(\n `INSERT INTO app_syncable_namespaces (app_id, tables_json, files_enabled)\n VALUES (?, ?, ?)\n ON CONFLICT(app_id) DO UPDATE SET\n tables_json = excluded.tables_json,\n files_enabled = excluded.files_enabled`,\n ).run(appId, JSON.stringify(tables), filesEnabled ? 1 : 0);\n}\n\nexport function deleteAppSyncableNamespace(db: DatabaseSync, appId: string): void {\n db.prepare(\"DELETE FROM app_syncable_namespaces WHERE app_id = ?\").run(appId);\n}\n\nexport function getAppSyncableNamespace(\n db: DatabaseSync,\n appId: string,\n): AppSyncableNamespace | null {\n const row = db\n .prepare(\n \"SELECT app_id, tables_json, files_enabled FROM app_syncable_namespaces WHERE app_id = ?\",\n )\n .get(appId) as { app_id: string; tables_json: string; files_enabled: number } | undefined;\n if (!row) return null;\n return rowToNamespace(row);\n}\n\nexport function listAppSyncableNamespaces(db: DatabaseSync): AppSyncableNamespace[] {\n const rows = db\n .prepare(\n \"SELECT app_id, tables_json, files_enabled FROM app_syncable_namespaces\",\n )\n .all() as Array<{ app_id: string; tables_json: string; files_enabled: number }>;\n return rows.map(rowToNamespace);\n}\n\n/**\n * Object-oriented wrapper over the free functions above — implements the\n * `AppSyncableNamespaceStore` interface required by `createAppSpecificFactory`\n * and the sync applier.\n */\nexport class SqliteAppSyncableNamespaceStore implements AppSyncableNamespaceStore {\n constructor(private readonly db: DatabaseSync) {}\n\n get(appId: string): AppSyncableNamespace | null {\n return getAppSyncableNamespace(this.db, appId);\n }\n\n list(): AppSyncableNamespace[] {\n return listAppSyncableNamespaces(this.db);\n }\n}\n","import type { DatabaseSync } from \"node:sqlite\";\nimport { serializeHLC, deserializeHLC } from \"@starkeep/protocol-primitives\";\nimport type { AppSyncableApplier, AppSyncableRowEntry, AppSyncableNamespaceStore, ScanCapableApplier, ScanSinceOptions, ScanSincePage } from \"@starkeep/shared-space-api\";\nimport { appSyncableTableName } from \"./namespace.js\";\n\n/**\n * SQLite-backed implementation of `AppSyncableApplier`.\n *\n * All writes use the LWW (last-write-wins) rule based on the HLC-serialized\n * `updated_at` column: an incoming entry is only applied if its timestamp is\n * strictly greater than the row's current `updated_at`. This makes the applier\n * idempotent — replaying the same entry twice is a no-op.\n *\n * Delete is soft: the `deleted_at` column is set rather than removing the row\n * so that the inline-HLC pull path can propagate tombstones to other clients.\n */\nexport class SqliteAppSyncableApplier\n implements AppSyncableApplier, ScanCapableApplier\n{\n constructor(\n private readonly db: DatabaseSync,\n private readonly namespace: AppSyncableNamespaceStore,\n ) {}\n\n apply(entry: AppSyncableRowEntry): void {\n const ns = this.namespace.get(entry.appId);\n if (!ns) {\n throw new Error(\n `SqliteAppSyncableApplier: app \"${entry.appId}\" not installed`,\n );\n }\n const tableInfo = ns.tables.find((t) => t.name === entry.table);\n if (!tableInfo) {\n throw new Error(\n `SqliteAppSyncableApplier: table \"${entry.table}\" not declared for app \"${entry.appId}\"`,\n );\n }\n\n const fullName = appSyncableTableName(entry.appId, entry.table);\n const { pkColumns } = tableInfo;\n\n if (entry.op === \"insert\") {\n this.applyInsert(fullName, pkColumns, entry);\n } else if (entry.op === \"update\") {\n this.applyUpdate(fullName, entry);\n } else {\n this.applyDelete(fullName, entry);\n }\n }\n\n private applyInsert(\n fullName: string,\n pkColumns: string[],\n entry: AppSyncableRowEntry,\n ): void {\n const row = entry.row ?? {};\n const cols = Object.keys(row);\n if (cols.length === 0) return;\n\n const colList = cols.map(q).join(\", \");\n const placeholders = cols.map(() => \"?\").join(\", \");\n const values = cols.map((c) => row[c] as unknown);\n\n if (pkColumns.length === 0) {\n // No PK declared — just insert, ignoring duplicates.\n this.db\n .prepare(`INSERT OR IGNORE INTO ${q(fullName)} (${colList}) VALUES (${placeholders})`)\n .run(...(values as never[]));\n return;\n }\n\n // UPSERT with LWW: only overwrite if the incoming updated_at is newer.\n const conflictTarget = pkColumns.map(q).join(\", \");\n const updateCols = cols.filter((c) => !pkColumns.includes(c));\n if (updateCols.length === 0) {\n this.db\n .prepare(\n `INSERT OR IGNORE INTO ${q(fullName)} (${colList}) VALUES (${placeholders})`,\n )\n .run(...(values as never[]));\n return;\n }\n const setClause = updateCols.map((c) => `${q(c)} = excluded.${q(c)}`).join(\", \");\n this.db\n .prepare(\n `INSERT INTO ${q(fullName)} (${colList}) VALUES (${placeholders})\n ON CONFLICT(${conflictTarget}) DO UPDATE SET ${setClause}\n WHERE excluded.updated_at > ${q(fullName)}.updated_at`,\n )\n .run(...(values as never[]));\n }\n\n private applyUpdate(fullName: string, entry: AppSyncableRowEntry): void {\n const patch = entry.row ?? {};\n const where = entry.where ?? {};\n const patchCols = Object.keys(patch);\n const whereCols = Object.keys(where);\n if (patchCols.length === 0) return;\n\n const setClause = patchCols.map((c) => `${q(c)} = ?`).join(\", \");\n // Only apply if the incoming updated_at is strictly newer (LWW).\n const incomingUpdatedAt = patch[\"updated_at\"] as string | undefined;\n const conditions: string[] = whereCols.map((c) => `${q(c)} = ?`);\n if (incomingUpdatedAt) conditions.push(`updated_at < ?`);\n const whereClause = conditions.length ? \" WHERE \" + conditions.join(\" AND \") : \"\";\n\n const params: unknown[] = [\n ...patchCols.map((c) => patch[c]),\n ...whereCols.map((c) => where[c]),\n ];\n if (incomingUpdatedAt) params.push(incomingUpdatedAt);\n\n this.db\n .prepare(`UPDATE ${q(fullName)} SET ${setClause}${whereClause}`)\n .run(...(params as never[]));\n }\n\n private applyDelete(fullName: string, entry: AppSyncableRowEntry): void {\n const where = entry.where ?? {};\n const whereCols = Object.keys(where);\n // Soft-delete: set deleted_at and updated_at.\n const incomingUpdatedAt = entry.row?.[\"updated_at\"] as string | undefined;\n const ts = incomingUpdatedAt ?? serializeHLC(entry.timestamp);\n\n const conditions: string[] = [\n ...whereCols.map((c) => `${q(c)} = ?`),\n `(updated_at IS NULL OR updated_at < ?)`,\n ];\n const whereClause = \" WHERE \" + conditions.join(\" AND \");\n\n // params order: SET deleted_at=?, updated_at=?, then WHERE bindings\n const params: unknown[] = [\n ts,\n ts,\n ...whereCols.map((c) => where[c]),\n ts, // for the LWW updated_at < ? condition\n ];\n\n this.db\n .prepare(\n `UPDATE ${q(fullName)} SET deleted_at = ?, updated_at = ?${whereClause}`,\n )\n .run(...(params as never[]));\n }\n\n /**\n * Pull-side synthesis: return rows updated after `sinceHlcStr` (or `cursor`\n * if higher) in HLC order, paginated. `updated_at` is a serialized HLC\n * whose lexicographic order matches HLC order (fixed-width hex), and each\n * row's HLC is unique per node, so it doubles as the cursor — no separate\n * tiebreaker column is needed.\n */\n async scanSince(\n appId: string,\n table: string,\n sinceHlcStr: string,\n options?: ScanSinceOptions,\n ): Promise<ScanSincePage> {\n const fullName = appSyncableTableName(appId, table);\n const floor =\n options?.cursor !== undefined && options.cursor > sinceHlcStr\n ? options.cursor\n : sinceHlcStr;\n const limit = options?.limit;\n let rows: Record<string, unknown>[];\n try {\n if (limit !== undefined) {\n rows = this.db\n .prepare(\n `SELECT * FROM ${q(fullName)} WHERE updated_at > ? ORDER BY updated_at ASC LIMIT ?`,\n )\n .all(floor, limit + 1) as Record<string, unknown>[];\n } else {\n rows = this.db\n .prepare(\n `SELECT * FROM ${q(fullName)} WHERE updated_at > ? ORDER BY updated_at ASC`,\n )\n .all(floor) as Record<string, unknown>[];\n }\n } catch {\n // Table might not exist yet (app not installed locally).\n return { rows: [], nextCursor: null, hasMore: false };\n }\n const hasMore = limit !== undefined && rows.length > limit;\n const pageRows = hasMore ? rows.slice(0, limit) : rows;\n const entries = pageRows.map((row) => rowToEntry(appId, table, row));\n const nextCursor =\n hasMore && pageRows.length > 0\n ? (pageRows[pageRows.length - 1]![\"updated_at\"] as string)\n : null;\n return { rows: entries, nextCursor, hasMore };\n }\n\n /** Support read path from the factory's queryRows. */\n queryRows(\n appId: string,\n table: string,\n where?: Record<string, unknown>,\n ): Record<string, unknown>[] {\n const fullName = appSyncableTableName(appId, table);\n const whereCols = where ? Object.keys(where) : [];\n // Filter out soft-deleted rows by default.\n const whereClause = [\n \"deleted_at IS NULL\",\n ...whereCols.map((c) => `${q(c)} = ?`),\n ].join(\" AND \");\n return this.db\n .prepare(`SELECT * FROM ${q(fullName)} WHERE ${whereClause}`)\n .all(...(whereCols.map((c) => where![c]) as never[])) as Record<string, unknown>[];\n }\n}\n\nfunction q(name: string): string {\n return `\"${name}\"`;\n}\n\nfunction rowToEntry(\n appId: string,\n table: string,\n row: Record<string, unknown>,\n): AppSyncableRowEntry {\n const updatedAtStr = row[\"updated_at\"] as string;\n const deletedAtStr = row[\"deleted_at\"] as string | null | undefined;\n const timestamp = deserializeHLC(updatedAtStr);\n\n // For wire propagation we emit op=\"insert\" (upsert) for live rows so the\n // receiver creates the row if it doesn't exist yet. op=\"update\" only\n // targets existing rows and would silently drop new rows during initial\n // sync. Tombstones still ride the soft-delete path.\n if (deletedAtStr) {\n return { timestamp, appId, table, op: \"delete\", row };\n }\n return { timestamp, appId, table, op: \"insert\", row };\n}\n"],"mappings":"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","names":["serializeHLC","serializeHLC","createStarkeepId","deserializeHLC","serializeHLC","serializeHLC","deserializeHLC","serializeHLC","deserializeHLC"]}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@starkeep/storage-sqlite",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./src/index.ts",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/starkeep-dev/starkeep-core.git",
26
+ "directory": "packages/storage-sqlite"
27
+ },
28
+ "homepage": "https://github.com/starkeep-dev/starkeep-core/tree/main/packages/storage-sqlite",
29
+ "dependencies": {
30
+ "kysely": "^0.27.0",
31
+ "@starkeep/access-control": "0.1.0",
32
+ "@starkeep/storage-adapter": "0.1.0",
33
+ "@starkeep/shared-space-api": "0.1.0",
34
+ "@starkeep/protocol-primitives": "0.1.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^25.4.0",
38
+ "tsup": "^8.0.0",
39
+ "typescript": "^5.7.0",
40
+ "vitest": "^3.0.0"
41
+ },
42
+ "scripts": {
43
+ "build": "tsup",
44
+ "test": "vitest run",
45
+ "typecheck": "tsc --noEmit",
46
+ "lint": "eslint src/"
47
+ }
48
+ }