chishiki-sqlite 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Seo Jong Hak
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # SQLite Package
2
+
3
+ This package provides SQLite with OPFS (Origin Private File System) support for the Chrome extension.
4
+
5
+ ## Features
6
+
7
+ - SQLite database with OPFS persistence
8
+ - Worker-based implementation for OPFS VFS
9
+ - React hooks for easy integration
10
+ - Client API for database operations
11
+
12
+ ## Usage
13
+
14
+ ### In a React Component
15
+
16
+ ```tsx
17
+ import { useSqliteOpfs } from '@extension/sqlite';
18
+
19
+ const MyComponent = () => {
20
+ const {
21
+ isReady,
22
+ db,
23
+ exec,
24
+ openDatabase,
25
+ closeDatabase,
26
+ } = useSqliteOpfs();
27
+
28
+ const handleQuery = async () => {
29
+ const result = await exec('SELECT * FROM table;');
30
+ console.log(result);
31
+ };
32
+
33
+ return (
34
+ <div>
35
+ {isReady && <button onClick={handleQuery}>Run Query</button>}
36
+ </div>
37
+ );
38
+ };
39
+ ```
40
+
41
+ ### Direct Client Usage
42
+
43
+ ```tsx
44
+ import { SqliteWorkerClient } from '@extension/sqlite';
45
+
46
+ const client = new SqliteWorkerClient();
47
+ await client.init();
48
+ await client.open('mydb.sqlite3');
49
+ const result = await client.exec('SELECT * FROM table;');
50
+ await client.close();
51
+ ```
52
+
53
+ ## Requirements
54
+
55
+ - OPFS support (Chrome 109+, Edge 109+)
56
+ - Background service worker (for worker context)
57
+ - Storage permissions in manifest
58
+
59
+ ## API
60
+
61
+ ### useSqliteOpfs()
62
+
63
+ React hook for SQLite operations.
64
+
65
+ **Returns:**
66
+ - `isReady`: boolean - WASM initialized
67
+ - `db`: boolean - Database is open
68
+ - `dbFilename`: string - Current database filename
69
+ - `opfsFiles`: Array - List of OPFS files
70
+ - `opfsSupported`: boolean - OPFS available
71
+ - `persistent`: boolean - Using persistent storage
72
+ - `storage`: 'opfs' | 'jsstorage' | 'memory' - Storage type
73
+ - `openDatabase()`: Function - Open database
74
+ - `closeDatabase()`: Function - Close database
75
+ - `exec()`: Function - Execute SQL
76
+ - `runSampleSchema()`: Function - Seed sample data
77
+ - `listOpfsFiles()`: Function - List OPFS files
78
+ - `deleteOpfsFile()`: Function - Delete file
79
+
80
+ ### SqliteWorkerClient
81
+
82
+ Direct client for SQLite operations.
83
+
84
+ **Methods:**
85
+ - `init()`: Initialize SQLite
86
+ - `open(filename)`: Open database
87
+ - `close()`: Close database
88
+ - `exec(sql)`: Execute SQL
89
+ - `seed()`: Seed sample schema
90
+ - `flush()`: Flush to disk
91
+ - `listOpfs()`: List OPFS files
92
+ - `deleteOpfs(filename)`: Delete file
93
+ - `dispose()`: Cleanup
94
+
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ export * from './lib/index.js';
@@ -0,0 +1,210 @@
1
+ import { getDatabaseManager } from './database-manager.js';
2
+ import { ManualSqliteClient } from './manual.js';
3
+ const getDb = () => {
4
+ const client = ManualSqliteClient.getInstance();
5
+ return client.getDb();
6
+ };
7
+ const ensureDatabaseInitialized = async () => {
8
+ const dbManager = getDatabaseManager();
9
+ await dbManager.ensureInitialized();
10
+ };
11
+ /**
12
+ * Initialize the activities table
13
+ */
14
+ const initializeActivitiesTable = async () => {
15
+ const db = getDb();
16
+ if (!db)
17
+ return;
18
+ const tableExists = await db.introspection
19
+ .getTables()
20
+ .then((tables) => tables.some((table) => table.name === 'activities'));
21
+ if (!tableExists) {
22
+ await db.schema
23
+ .createTable('activities')
24
+ .ifNotExists()
25
+ .addColumn('id', 'integer', col => col.primaryKey().autoIncrement())
26
+ .addColumn('activity_iri', 'text', col => col.notNull().unique())
27
+ .addColumn('name', 'text', col => col.notNull())
28
+ .addColumn('description', 'text')
29
+ .addColumn('activity_type', 'text', col => col.notNull())
30
+ .addColumn('content_id', 'integer')
31
+ .addColumn('created_at', 'text', col => col.notNull())
32
+ .execute();
33
+ // Create indexes
34
+ await db.schema.createIndex('idx_activities_iri').ifNotExists().on('activities').column('activity_iri').execute();
35
+ await db.schema.createIndex('idx_activities_content').ifNotExists().on('activities').column('content_id').execute();
36
+ await db.schema.createIndex('idx_activities_type').ifNotExists().on('activities').column('activity_type').execute();
37
+ }
38
+ };
39
+ /**
40
+ * Generate a unique activity IRI
41
+ */
42
+ const generateActivityIri = (type, id) => {
43
+ const timestamp = Date.now();
44
+ const random = Math.random().toString(36).substring(2, 8);
45
+ const suffix = id || `${timestamp}-${random}`;
46
+ return `https://chishiki.app/activities/${type}/${suffix}`;
47
+ };
48
+ /**
49
+ * Create a new activity
50
+ */
51
+ const createActivity = async (activity) => {
52
+ await ensureDatabaseInitialized();
53
+ const db = getDb();
54
+ if (!db) {
55
+ throw new Error('Database not available');
56
+ }
57
+ const now = new Date().toISOString();
58
+ const iri = activity.activity_iri || generateActivityIri(activity.activity_type);
59
+ const insertValues = {
60
+ activity_iri: iri,
61
+ name: activity.name,
62
+ description: activity.description ?? null,
63
+ activity_type: activity.activity_type,
64
+ content_id: activity.content_id ?? null,
65
+ created_at: now,
66
+ };
67
+ const result = await db.insertInto('activities').values(insertValues).returningAll().executeTakeFirstOrThrow();
68
+ return result;
69
+ };
70
+ /**
71
+ * Get activity by ID
72
+ */
73
+ const getActivityById = async (id) => {
74
+ await ensureDatabaseInitialized();
75
+ const db = getDb();
76
+ if (!db)
77
+ return null;
78
+ const result = await db.selectFrom('activities').selectAll().where('id', '=', id).executeTakeFirst();
79
+ return result || null;
80
+ };
81
+ /**
82
+ * Get activity by IRI
83
+ */
84
+ const getActivityByIri = async (iri) => {
85
+ await ensureDatabaseInitialized();
86
+ const db = getDb();
87
+ if (!db)
88
+ return null;
89
+ const result = await db.selectFrom('activities').selectAll().where('activity_iri', '=', iri).executeTakeFirst();
90
+ return result || null;
91
+ };
92
+ /**
93
+ * Get or create an activity by IRI
94
+ */
95
+ const getOrCreateActivity = async (activity) => {
96
+ const existing = await getActivityByIri(activity.activity_iri);
97
+ if (existing) {
98
+ return existing;
99
+ }
100
+ return createActivity(activity);
101
+ };
102
+ /**
103
+ * Get activities for a content item
104
+ */
105
+ const getActivitiesByContentId = async (contentId) => {
106
+ await ensureDatabaseInitialized();
107
+ const db = getDb();
108
+ if (!db)
109
+ return [];
110
+ const results = await db
111
+ .selectFrom('activities')
112
+ .selectAll()
113
+ .where('content_id', '=', contentId)
114
+ .orderBy('created_at', 'desc')
115
+ .execute();
116
+ return results;
117
+ };
118
+ /**
119
+ * Get activities by type
120
+ */
121
+ const getActivitiesByType = async (activityType) => {
122
+ await ensureDatabaseInitialized();
123
+ const db = getDb();
124
+ if (!db)
125
+ return [];
126
+ const results = await db
127
+ .selectFrom('activities')
128
+ .selectAll()
129
+ .where('activity_type', '=', activityType)
130
+ .orderBy('created_at', 'desc')
131
+ .execute();
132
+ return results;
133
+ };
134
+ /**
135
+ * Get all activities with pagination
136
+ */
137
+ const getActivities = async (page = 1, limit = 20) => {
138
+ await ensureDatabaseInitialized();
139
+ const db = getDb();
140
+ if (!db)
141
+ return [];
142
+ const results = await db
143
+ .selectFrom('activities')
144
+ .selectAll()
145
+ .orderBy('created_at', 'desc')
146
+ .offset((page - 1) * limit)
147
+ .limit(limit)
148
+ .execute();
149
+ return results;
150
+ };
151
+ /**
152
+ * Get activity count
153
+ */
154
+ const getActivityCount = async () => {
155
+ await ensureDatabaseInitialized();
156
+ const db = getDb();
157
+ if (!db)
158
+ return 0;
159
+ const result = await db.selectFrom('activities').select(db.fn.count('id').as('count')).executeTakeFirst();
160
+ return result?.count || 0;
161
+ };
162
+ /**
163
+ * Update an activity
164
+ */
165
+ const updateActivity = async (id, updates) => {
166
+ await ensureDatabaseInitialized();
167
+ const db = getDb();
168
+ if (!db)
169
+ return;
170
+ const updateValues = {};
171
+ if (updates.name !== undefined)
172
+ updateValues.name = updates.name;
173
+ if (updates.description !== undefined)
174
+ updateValues.description = updates.description;
175
+ if (Object.keys(updateValues).length > 0) {
176
+ await db.updateTable('activities').set(updateValues).where('id', '=', id).execute();
177
+ }
178
+ };
179
+ /**
180
+ * Delete an activity
181
+ */
182
+ const deleteActivity = async (id) => {
183
+ await ensureDatabaseInitialized();
184
+ const db = getDb();
185
+ if (!db)
186
+ return;
187
+ await db.deleteFrom('activities').where('id', '=', id).execute();
188
+ };
189
+ /**
190
+ * Delete activities by content ID
191
+ */
192
+ const deleteActivitiesByContentId = async (contentId) => {
193
+ await ensureDatabaseInitialized();
194
+ const db = getDb();
195
+ if (!db)
196
+ return;
197
+ await db.deleteFrom('activities').where('content_id', '=', contentId).execute();
198
+ };
199
+ /**
200
+ * Reset the activities table
201
+ */
202
+ const resetActivitiesTable = async () => {
203
+ await ensureDatabaseInitialized();
204
+ const db = getDb();
205
+ if (!db)
206
+ return;
207
+ await db.schema.dropTable('activities').ifExists().execute();
208
+ await initializeActivitiesTable();
209
+ };
210
+ export { initializeActivitiesTable, generateActivityIri, createActivity, getActivityById, getActivityByIri, getOrCreateActivity, getActivitiesByContentId, getActivitiesByType, getActivities, getActivityCount, updateActivity, deleteActivity, deleteActivitiesByContentId, resetActivitiesTable, };
@@ -0,0 +1,147 @@
1
+ import { getDatabaseManager } from './database-manager.js';
2
+ import { ManualSqliteClient } from './manual.js';
3
+ const getDb = () => {
4
+ const client = ManualSqliteClient.getInstance();
5
+ return client.getDb();
6
+ };
7
+ const ensureDatabaseInitialized = async () => {
8
+ const dbManager = getDatabaseManager();
9
+ await dbManager.ensureInitialized();
10
+ };
11
+ /**
12
+ * Initialize the actors table
13
+ */
14
+ const initializeActorsTable = async () => {
15
+ const db = getDb();
16
+ if (!db)
17
+ return;
18
+ const tableExists = await db.introspection
19
+ .getTables()
20
+ .then((tables) => tables.some((table) => table.name === 'actors'));
21
+ if (!tableExists) {
22
+ await db.schema
23
+ .createTable('actors')
24
+ .ifNotExists()
25
+ .addColumn('id', 'integer', col => col.primaryKey().autoIncrement())
26
+ .addColumn('mbox', 'text', col => col.notNull().unique())
27
+ .addColumn('name', 'text', col => col.notNull())
28
+ .addColumn('created_at', 'text', col => col.notNull())
29
+ .execute();
30
+ // Create index on mbox for faster lookups
31
+ await db.schema.createIndex('idx_actors_mbox').ifNotExists().on('actors').column('mbox').execute();
32
+ }
33
+ };
34
+ /**
35
+ * Get or create the default local actor
36
+ */
37
+ const getOrCreateDefaultActor = async () => {
38
+ await ensureDatabaseInitialized();
39
+ const db = getDb();
40
+ if (!db) {
41
+ throw new Error('Database not available');
42
+ }
43
+ const defaultMbox = 'mailto:local@chishiki.app';
44
+ const defaultName = 'Local User';
45
+ // Try to find existing default actor
46
+ const existing = await db.selectFrom('actors').selectAll().where('mbox', '=', defaultMbox).executeTakeFirst();
47
+ if (existing) {
48
+ return existing;
49
+ }
50
+ // Create default actor
51
+ const now = new Date().toISOString();
52
+ const result = await db
53
+ .insertInto('actors')
54
+ .values({
55
+ mbox: defaultMbox,
56
+ name: defaultName,
57
+ created_at: now,
58
+ })
59
+ .returningAll()
60
+ .executeTakeFirstOrThrow();
61
+ return result;
62
+ };
63
+ /**
64
+ * Get actor by ID
65
+ */
66
+ const getActorById = async (id) => {
67
+ await ensureDatabaseInitialized();
68
+ const db = getDb();
69
+ if (!db)
70
+ return null;
71
+ const result = await db.selectFrom('actors').selectAll().where('id', '=', id).executeTakeFirst();
72
+ return result || null;
73
+ };
74
+ /**
75
+ * Get actor by mbox (email identifier)
76
+ */
77
+ const getActorByMbox = async (mbox) => {
78
+ await ensureDatabaseInitialized();
79
+ const db = getDb();
80
+ if (!db)
81
+ return null;
82
+ const result = await db.selectFrom('actors').selectAll().where('mbox', '=', mbox).executeTakeFirst();
83
+ return result || null;
84
+ };
85
+ /**
86
+ * Create a new actor
87
+ */
88
+ const createActor = async (actor) => {
89
+ await ensureDatabaseInitialized();
90
+ const db = getDb();
91
+ if (!db) {
92
+ throw new Error('Database not available');
93
+ }
94
+ const now = new Date().toISOString();
95
+ const result = await db
96
+ .insertInto('actors')
97
+ .values({
98
+ ...actor,
99
+ created_at: now,
100
+ })
101
+ .returningAll()
102
+ .executeTakeFirstOrThrow();
103
+ return result;
104
+ };
105
+ /**
106
+ * Update an actor's name
107
+ */
108
+ const updateActorName = async (id, name) => {
109
+ await ensureDatabaseInitialized();
110
+ const db = getDb();
111
+ if (!db)
112
+ return;
113
+ await db.updateTable('actors').set({ name }).where('id', '=', id).execute();
114
+ };
115
+ /**
116
+ * Get all actors
117
+ */
118
+ const getAllActors = async () => {
119
+ await ensureDatabaseInitialized();
120
+ const db = getDb();
121
+ if (!db)
122
+ return [];
123
+ const results = await db.selectFrom('actors').selectAll().orderBy('created_at', 'desc').execute();
124
+ return results;
125
+ };
126
+ /**
127
+ * Delete an actor (use with caution - will orphan statements)
128
+ */
129
+ const deleteActor = async (id) => {
130
+ await ensureDatabaseInitialized();
131
+ const db = getDb();
132
+ if (!db)
133
+ return;
134
+ await db.deleteFrom('actors').where('id', '=', id).execute();
135
+ };
136
+ /**
137
+ * Reset the actors table (for development/testing)
138
+ */
139
+ const resetActorsTable = async () => {
140
+ await ensureDatabaseInitialized();
141
+ const db = getDb();
142
+ if (!db)
143
+ return;
144
+ await db.schema.dropTable('actors').ifExists().execute();
145
+ await initializeActorsTable();
146
+ };
147
+ export { initializeActorsTable, getOrCreateDefaultActor, getActorById, getActorByMbox, createActor, updateActorName, getAllActors, deleteActor, resetActorsTable, };
@@ -0,0 +1,70 @@
1
+ // Database Manager - Singleton for centralized database initialization and management
2
+ import { initializeActivitiesTable } from './activities.js';
3
+ import { initializeActorsTable } from './actors.js';
4
+ import { initializeGeneratedActivitiesTable } from './generated-activities.js';
5
+ import { initializeLearningContentTable } from './learning-content.js';
6
+ import { ManualSqliteClient } from './manual.js';
7
+ import { initializeStatementsTable } from './statements.js';
8
+ export class DatabaseManager {
9
+ static instance = null;
10
+ initializationPromise = null;
11
+ isInitialized = false;
12
+ constructor() { }
13
+ static getInstance() {
14
+ if (!DatabaseManager.instance) {
15
+ DatabaseManager.instance = new DatabaseManager();
16
+ }
17
+ return DatabaseManager.instance;
18
+ }
19
+ /**
20
+ * Ensures the database is initialized. Returns a promise that resolves when initialization is complete.
21
+ * This method is idempotent - multiple calls will return the same promise.
22
+ */
23
+ async ensureInitialized() {
24
+ if (this.isInitialized) {
25
+ return;
26
+ }
27
+ if (!this.initializationPromise) {
28
+ this.initializationPromise = this.initialize();
29
+ }
30
+ await this.initializationPromise;
31
+ this.isInitialized = true;
32
+ }
33
+ /**
34
+ * Performs the actual database initialization
35
+ */
36
+ async initialize() {
37
+ try {
38
+ console.log('Initializing database...');
39
+ // Initialize all LRS database tables in parallel
40
+ await Promise.all([
41
+ initializeActorsTable(),
42
+ initializeLearningContentTable(),
43
+ initializeActivitiesTable(),
44
+ initializeStatementsTable(),
45
+ initializeGeneratedActivitiesTable(),
46
+ ]);
47
+ console.log('Database initialization completed');
48
+ }
49
+ catch (error) {
50
+ console.error('Database initialization failed:', error);
51
+ throw error;
52
+ }
53
+ }
54
+ /**
55
+ * Resets the initialization state (useful for testing or reinitialization)
56
+ */
57
+ reset() {
58
+ this.isInitialized = false;
59
+ this.initializationPromise = null;
60
+ ManualSqliteClient.resetInstance();
61
+ }
62
+ /**
63
+ * Checks if the database is currently initialized
64
+ */
65
+ get initialized() {
66
+ return this.isInitialized;
67
+ }
68
+ }
69
+ // Export a convenience function for easy access
70
+ export const getDatabaseManager = () => DatabaseManager.getInstance();
@@ -0,0 +1,29 @@
1
+ import { resetActivitiesTable } from './activities.js';
2
+ import { resetActorsTable } from './actors.js';
3
+ import { getDatabaseManager } from './database-manager.js';
4
+ import { resetGeneratedActivitiesTable } from './generated-activities.js';
5
+ import { resetLearningContentTable } from './learning-content.js';
6
+ import { resetStatementsTable } from './statements.js';
7
+ /**
8
+ * Completely resets the entire database by dropping and recreating all tables
9
+ * This will delete ALL data in the database
10
+ */
11
+ export const resetEntireDatabase = async () => {
12
+ console.log('Resetting entire database...');
13
+ try {
14
+ // Reset all LRS tables in order (statements first due to foreign keys)
15
+ await resetStatementsTable();
16
+ await resetGeneratedActivitiesTable();
17
+ await resetActivitiesTable();
18
+ await resetLearningContentTable();
19
+ await resetActorsTable();
20
+ // Reset the database manager singleton state
21
+ const dbManager = getDatabaseManager();
22
+ dbManager.reset();
23
+ console.log('Database reset completed successfully');
24
+ }
25
+ catch (error) {
26
+ console.error('Error resetting database:', error);
27
+ throw error;
28
+ }
29
+ };