From ee7aa5832db14d3a42609d781bb88badead5beb9 Mon Sep 17 00:00:00 2001 From: Dallas Hoffman Date: Sat, 20 Jul 2024 22:09:20 -0400 Subject: [PATCH] SQLocal constructor accepts additional options: readOnly and verbose --- docs/guide/setup.md | 16 ++++++++++++++ src/client.ts | 12 ++++++---- src/processor.ts | 29 ++++++++++++------------ src/types.ts | 11 +++++---- test/init.test.ts | 54 +++++++++++++++++++++++++++++++++++++++++++++ test/sql.test.ts | 4 ---- 6 files changed, 99 insertions(+), 27 deletions(-) create mode 100644 test/init.test.ts diff --git a/docs/guide/setup.md b/docs/guide/setup.md index f0001b6..13934a3 100644 --- a/docs/guide/setup.md +++ b/docs/guide/setup.md @@ -55,6 +55,22 @@ With the client initialized, you are ready to [start making queries](/api/sql). +## Options + +The `SQLocal` constructor can also be passed an object to accept additional options. + +```javascript +export const db = new SQLocal({ + databasePath: 'database.sqlite3', + readOnly: true, + verbose: true, +}); +``` + +- **`databasePath`** (`string`) - The file name for the database file. This is the only required option. +- **`readOnly`** (`boolean`) - If `true`, connect to the database in read-only mode. Attempts to run queries that would mutate the database will throw an error. +- **`verbose`** (`boolean`) - If `true`, any SQL executed on the database will be logged to the console. + ## Vite Configuration Vite currently has an issue that prevents it from loading web worker files correctly with the default configuration. If you use Vite, please add the below to your [Vite configuration](https://vitejs.dev/config/) to fix this. Don't worry: it will have no impact on production performance. diff --git a/src/client.ts b/src/client.ts index 232c05f..66f95f9 100644 --- a/src/client.ts +++ b/src/client.ts @@ -18,6 +18,7 @@ import type { GetInfoMessage, Statement, DatabaseInfo, + ClientConfig, } from './types.js'; import { sqlTag } from './lib/sql-tag.js'; import { convertRowsToObjects } from './lib/convert-rows-to-objects.js'; @@ -36,18 +37,21 @@ export class SQLocal { ] >(); - constructor(databasePath: string) { + constructor(databasePath: string); + constructor(config: ClientConfig); + constructor(config: string | ClientConfig) { + config = typeof config === 'string' ? { databasePath: config } : config; + this.worker = new Worker(new URL('./worker', import.meta.url), { type: 'module', }); this.worker.addEventListener('message', this.processMessageEvent); this.proxy = coincident(this.worker) as WorkerProxy; - this.databasePath = databasePath; + this.databasePath = config.databasePath; this.worker.postMessage({ type: 'config', - key: 'databasePath', - value: databasePath, + config, } satisfies ConfigMessage); } diff --git a/src/processor.ts b/src/processor.ts index 7117878..79a0d41 100644 --- a/src/processor.ts +++ b/src/processor.ts @@ -17,6 +17,7 @@ import type { RawResultData, GetInfoMessage, Sqlite3StorageType, + ConfigMessage, } from './types.js'; export class SQLocalProcessor { @@ -38,6 +39,12 @@ export class SQLocalProcessor { protected init = async (): Promise => { if (!this.config.databasePath) return; + const { databasePath, readOnly, verbose } = this.config; + const flags = [ + readOnly === true ? 'r' : 'cw', + verbose === true ? 't' : '', + ].join(''); + try { if (!this.sqlite3) { this.sqlite3 = await sqlite3InitModule(); @@ -48,13 +55,13 @@ export class SQLocalProcessor { } if ('opfs' in this.sqlite3) { - this.db = new this.sqlite3.oo1.OpfsDb(this.config.databasePath, 'cw'); + this.db = new this.sqlite3.oo1.OpfsDb(databasePath, flags); this.dbStorageType = 'opfs'; } else { - this.db = new this.sqlite3.oo1.DB(this.config.databasePath, 'cw'); + this.db = new this.sqlite3.oo1.DB(databasePath, flags); this.dbStorageType = 'memory'; console.warn( - `The origin private file system is not available, so ${this.config.databasePath} will not be persisted. Make sure your web server is configured to use the correct HTTP response headers (See https://sqlocal.dallashoffman.com/guide/setup#cross-origin-isolation).` + `The origin private file system is not available, so ${databasePath} will not be persisted. Make sure your web server is configured to use the correct HTTP response headers (See https://sqlocal.dallashoffman.com/guide/setup#cross-origin-isolation).` ); } } catch (error) { @@ -84,7 +91,7 @@ export class SQLocalProcessor { switch (message.type) { case 'config': - this.editConfig(message.key, message.value); + this.editConfig(message); break; case 'query': case 'batch': @@ -111,17 +118,9 @@ export class SQLocalProcessor { } }; - protected editConfig = ( - key: T, - value: ProcessorConfig[T] - ): void => { - if (this.config[key] === value) return; - - this.config[key] = value; - - if (key === 'databasePath') { - this.init(); - } + protected editConfig = (message: ConfigMessage) => { + this.config = message.config; + this.init(); }; protected exec = (message: QueryMessage | BatchMessage): void => { diff --git a/src/types.ts b/src/types.ts index b8b153b..42077b7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -28,10 +28,14 @@ export type RawResultData = { // Database status -export type ProcessorConfig = { - databasePath?: string; +export type ClientConfig = { + databasePath: string; + readOnly?: boolean; + verbose?: boolean; }; +export type ProcessorConfig = Partial; + export type DatabaseInfo = { databasePath?: string; databaseSizeBytes?: number; @@ -79,8 +83,7 @@ export type FunctionMessage = { }; export type ConfigMessage = { type: 'config'; - key: keyof ProcessorConfig; - value: any; + config: ProcessorConfig; }; export type ImportMessage = { type: 'import'; diff --git a/test/init.test.ts b/test/init.test.ts new file mode 100644 index 0000000..00a61e0 --- /dev/null +++ b/test/init.test.ts @@ -0,0 +1,54 @@ +import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { SQLocal } from '../src/index'; + +describe('init', () => { + const databasePath = 'init-test.sqlite3'; + const { sql } = new SQLocal({ databasePath }); + + beforeEach(async () => { + await sql`CREATE TABLE nums (num INTEGER NOT NULL)`; + await sql`INSERT INTO nums (num) VALUES (0)`; + }); + + afterEach(async () => { + await sql`DROP TABLE nums`; + }); + + afterAll(async () => { + const opfs = await navigator.storage.getDirectory(); + await opfs.removeEntry(databasePath); + }); + + it('should be cross-origin isolated', () => { + expect(crossOriginIsolated).toBe(true); + }); + + it('should create a file in the OPFS', async () => { + const opfs = await navigator.storage.getDirectory(); + const fileHandle = await opfs.getFileHandle(databasePath); + const file = await fileHandle.getFile(); + expect(file.size).toBeGreaterThan(0); + }); + + it('should enable read-only mode', async () => { + const { sql, destroy } = new SQLocal({ + databasePath, + readOnly: true, + }); + + const write = async () => { + await sql`INSERT INTO nums (num) VALUES (1)`; + }; + expect(write).rejects.toThrowError( + 'SQLITE_IOERR_WRITE: sqlite3 result code 778: disk I/O error' + ); + + const read = async () => { + return await sql`SELECT * FROM nums`; + }; + const data = await read(); + expect(data).toEqual([{ num: 0 }]); + + await destroy(); + }); +}); diff --git a/test/sql.test.ts b/test/sql.test.ts index b80b6aa..6a4f105 100644 --- a/test/sql.test.ts +++ b/test/sql.test.ts @@ -49,8 +49,4 @@ describe('sql', () => { const select3 = await sql(sqlStr, 1); expect(select3).toEqual([{ name: 'bread' }]); }); - - it('should be cross-origin isolated', () => { - expect(crossOriginIsolated).toBe(true); - }); });