diff --git a/src/index.ts b/src/index.ts index a01840f..f968008 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,290 @@ import { NativeModules, Platform } from 'react-native'; +export type Scalar = + | string + | number + | boolean + | null + | ArrayBuffer + | ArrayBufferView; + +/** + * Object returned by SQL Query executions { + * insertId: Represent the auto-generated row id if applicable + * rowsAffected: Number of affected rows if result of a update query + * message: if status === 1, here you will find error description + * rows: if status is undefined or 0 this object will contain the query results + * } + * + * @interface QueryResult + */ +export type QueryResult = { + insertId?: number; + rowsAffected: number; + res?: any[]; + rows: Array<Record<string, Scalar>>; + // An array of intermediate results, just values without column names + rawRows?: Scalar[][]; + columnNames?: string[]; + /** + * Query metadata, available only for select query results + */ + metadata?: ColumnMetadata[]; +}; + +/** + * Column metadata + * Describes some information about columns fetched by the query + */ +export type ColumnMetadata = { + /** The name used for this column for this result set */ + name: string; + /** The declared column type for this column, when fetched directly from a table or a View resulting from a table column. "UNKNOWN" for dynamic values, like function returned ones. */ + type: string; + /** + * The index for this column for this result set*/ + index: number; +}; + +/** + * Allows the execution of bulk of sql commands + * inside a transaction + * If a single query must be executed many times with different arguments, its preferred + * to declare it a single time, and use an array of array parameters. + */ +export type SQLBatchTuple = [string] | [string, Array<any> | Array<Array<any>>]; + +export type UpdateHookOperation = 'INSERT' | 'DELETE' | 'UPDATE'; + +/** + * status: 0 or undefined for correct execution, 1 for error + * message: if status === 1, here you will find error description + * rowsAffected: Number of affected rows if status == 0 + */ +export type BatchQueryResult = { + rowsAffected?: number; +}; + +/** + * Result of loading a file and executing every line as a SQL command + * Similar to BatchQueryResult + */ +export type FileLoadResult = BatchQueryResult & { + commands?: number; +}; + +export type Transaction = { + commit: () => Promise<QueryResult>; + execute: (query: string, params?: Scalar[]) => Promise<QueryResult>; + rollback: () => Promise<QueryResult>; +}; + +type PendingTransaction = { + /* + * The start function should not throw or return a promise because the + * queue just calls it and does not monitor for failures or completions. + * + * It should catch any errors and call the resolve or reject of the wrapping + * promise when complete. + * + * It should also automatically commit or rollback the transaction if needed + */ + start: () => void; +}; + +export type PreparedStatement = { + bind: (params: any[]) => Promise<void>; + execute: () => Promise<QueryResult>; +}; + +type InternalDB = { + close: () => void; + delete: (location?: string) => void; + attach: ( + mainDbName: string, + dbNameToAttach: string, + alias: string, + location?: string + ) => void; + detach: (mainDbName: string, alias: string) => void; + transaction: (fn: (tx: Transaction) => Promise<void>) => Promise<void>; + executeSync: (query: string, params?: Scalar[]) => QueryResult; + execute: (query: string, params?: Scalar[]) => Promise<QueryResult>; + executeWithHostObjects: ( + query: string, + params?: Scalar[] + ) => Promise<QueryResult>; + executeBatch: (commands: SQLBatchTuple[]) => Promise<BatchQueryResult>; + loadFile: (location: string) => Promise<FileLoadResult>; + updateHook: ( + callback?: + | ((params: { + table: string; + operation: UpdateHookOperation; + row?: any; + rowId: number; + }) => void) + | null + ) => void; + commitHook: (callback?: (() => void) | null) => void; + rollbackHook: (callback?: (() => void) | null) => void; + prepareStatement: (query: string) => PreparedStatement; + loadExtension: (path: string, entryPoint?: string) => void; + executeRaw: (query: string, params?: Scalar[]) => Promise<any[]>; + getDbPath: (location?: string) => string; + reactiveExecute: (params: { + query: string; + arguments: any[]; + fireOn: { + table: string; + ids?: number[]; + }[]; + callback: (response: any) => void; + }) => () => void; + /** This function is only available for libsql. + * Allows to trigger a sync the database with it's remote replica + * In order for this function to work you need to use openSync or openRemote functions + * with libsql: true in the package.json + * + * The database is hosted in turso + **/ + sync: () => void; + flushPendingReactiveQueries: () => Promise<void>; +}; + +export type DB = { + close: () => void; + delete: (location?: string) => void; + attach: ( + mainDbName: string, + dbNameToAttach: string, + alias: string, + location?: string + ) => void; + detach: (mainDbName: string, alias: string) => void; + transaction: (fn: (tx: Transaction) => Promise<void>) => Promise<void>; + /** + * Sync version of the execute function + * It will block the JS thread and therefore your UI and should be used with caution + * + * When writing your queries, you can use the ? character as a placeholder for parameters + * The parameters will be automatically escaped and sanitized + * + * Example: + * db.executeSync('SELECT * FROM table WHERE id = ?', [1]); + * + * If you are writing a query that doesn't require parameters, you can omit the second argument + * + * If you are writing to the database YOU SHOULD BE USING TRANSACTIONS! + * Transactions protect you from partial writes and ensure that your data is always in a consistent state + * + * @param query + * @param params + * @returns QueryResult + */ + executeSync: (query: string, params?: Scalar[]) => QueryResult; + /** + * Basic query execution function, it is async don't forget to await it + * + * When writing your queries, you can use the ? character as a placeholder for parameters + * The parameters will be automatically escaped and sanitized + * + * Example: + * await db.execute('SELECT * FROM table WHERE id = ?', [1]); + * + * If you are writing a query that doesn't require parameters, you can omit the second argument + * + * If you are writing to the database YOU SHOULD BE USING TRANSACTIONS! + * Transactions protect you from partial writes and ensure that your data is always in a consistent state + * + * @param query string of your SQL query + * @param params a list of parameters to bind to the query, if any + * @returns Promise<QueryResult> with the result of the query + */ + execute: (query: string, params?: Scalar[]) => Promise<QueryResult>; + /** + * Similar to the execute function but returns the response in HostObjects + * Read more about HostObjects in the documentation and their pitfalls + * + * Will be a lot faster than the normal execute functions when returning data but you will pay when accessing the fields + * as the conversion is done the moment you access any field + * @param query + * @param params + * @returns + */ + executeWithHostObjects: ( + query: string, + params?: Scalar[] + ) => Promise<QueryResult>; + executeBatch: (commands: SQLBatchTuple[]) => Promise<BatchQueryResult>; + loadFile: (location: string) => Promise<FileLoadResult>; + updateHook: ( + callback?: + | ((params: { + table: string; + operation: UpdateHookOperation; + row?: any; + rowId: number; + }) => void) + | null + ) => void; + commitHook: (callback?: (() => void) | null) => void; + rollbackHook: (callback?: (() => void) | null) => void; + /** + * Constructs a prepared statement from the query string + * The statement can be re-bound with parameters and executed + * The performance gain is significant when the same query is executed multiple times, NOT when the query is executed (once) + * The cost lies in the preparation of the statement as it is compiled and optimized by the sqlite engine, the params can then rebound + * but the query itself is already optimized + * + * @param query string of your SQL query + * @returns Prepared statement object + */ + prepareStatement: (query: string) => PreparedStatement; + loadExtension: (path: string, entryPoint?: string) => void; + executeRaw: (query: string, params?: Scalar[]) => Promise<any[]>; + getDbPath: (location?: string) => string; + reactiveExecute: (params: { + query: string; + arguments: any[]; + fireOn: { + table: string; + ids?: number[]; + }[]; + callback: (response: any) => void; + }) => () => void; + /** This function is only available for libsql. + * Allows to trigger a sync the database with it's remote replica + * In order for this function to work you need to use openSync or openRemote functions + * with libsql: true in the package.json + * + * The database is hosted in turso + **/ + sync: () => void; +}; + +export type DBParams = { + url?: string; + authToken?: string; + name?: string; + location?: string; + syncInterval?: number; +}; + +export type OPSQLiteProxy = { + open: (options: { + name: string; + location?: string; + encryptionKey?: string; + }) => InternalDB; + openRemote: (options: { url: string; authToken: string }) => InternalDB; + openSync: (options: DBParams) => InternalDB; + isSQLCipher: () => boolean; + isLibsql: () => boolean; + isIOSEmbedded: () => boolean; +}; + declare global { - function nativeCallSyncHook(): unknown; var __OPSQLiteProxy: object | undefined; } @@ -41,12 +324,7 @@ export const { ? NativeModules.OPSQLite.getConstants() : NativeModules.OPSQLite; -const locks: Record< - string, - { queue: PendingTransaction[]; inProgress: boolean } -> = {}; - -function enhanceDB(db: DB, options: any): DB { +function enhanceDB(db: InternalDB, options: DBParams): DB { const lock = { queue: [] as PendingTransaction[], inProgress: false, @@ -72,7 +350,8 @@ function enhanceDB(db: DB, options: any): DB { } }; - // spreading the object is not working, so we need to do it manually + // spreading the object does not work with HostObjects (db) + // We need to manually assign the fields let enhancedDb = { delete: db.delete, attach: db.attach, @@ -87,11 +366,7 @@ function enhanceDB(db: DB, options: any): DB { getDbPath: db.getDbPath, reactiveExecute: db.reactiveExecute, sync: db.sync, - flushPendingReactiveQueries: db.flushPendingReactiveQueries, - close: () => { - db.close(); - delete locks[options.url]; - }, + close: db.close, executeWithHostObjects: async ( query: string, params?: Scalar[] @@ -121,7 +396,7 @@ function enhanceDB(db: DB, options: any): DB { ? db.executeSync(query, sanitizedParams as Scalar[]) : db.executeSync(query); - let rows: any[] = []; + let rows: Record<string, Scalar>[] = []; for (let i = 0; i < (intermediateResult.rawRows?.length ?? 0); i++) { let row: Record<string, Scalar> = {}; let rawRow = intermediateResult.rawRows![i]!; @@ -160,9 +435,9 @@ function enhanceDB(db: DB, options: any): DB { sanitizedParams as Scalar[] ); - let rows: any[] = []; + let rows: Record<string, Scalar>[] = []; for (let i = 0; i < (intermediateResult.rawRows?.length ?? 0); i++) { - let row: any = {}; + let row: Record<string, Scalar> = {}; let rawRow = intermediateResult.rawRows![i]!; for (let j = 0; j < intermediateResult.columnNames!.length; j++) { let columnName = intermediateResult.columnNames![j]!; @@ -186,7 +461,7 @@ function enhanceDB(db: DB, options: any): DB { const stmt = db.prepareStatement(query); return { - bind: async (params: any[]) => { + bind: async (params: Scalar[]) => { const sanitizedParams = params.map((p) => { if (ArrayBuffer.isView(p)) { return p.buffer; @@ -205,10 +480,12 @@ function enhanceDB(db: DB, options: any): DB { ): Promise<void> => { let isFinalized = false; - const execute = async (query: string, params?: any[] | undefined) => { + const execute = async (query: string, params?: Scalar[]) => { if (isFinalized) { throw Error( - `OP-Sqlite Error: Database: ${options.url}. Cannot execute query on finalized transaction` + `OP-Sqlite Error: Database: ${ + options.name || options.url + }. Cannot execute query on finalized transaction` ); } return await enhancedDb.execute(query, params); @@ -217,12 +494,14 @@ function enhanceDB(db: DB, options: any): DB { const commit = async (): Promise<QueryResult> => { if (isFinalized) { throw Error( - `OP-Sqlite Error: Database: ${options.url}. Cannot execute query on finalized transaction` + `OP-Sqlite Error: Database: ${ + options.name || options.url + }. Cannot execute query on finalized transaction` ); } const result = await enhancedDb.execute('COMMIT;'); - await enhancedDb.flushPendingReactiveQueries(); + await db.flushPendingReactiveQueries(); isFinalized = true; return result; @@ -231,7 +510,9 @@ function enhanceDB(db: DB, options: any): DB { const rollback = async (): Promise<QueryResult> => { if (isFinalized) { throw Error( - `OP-Sqlite Error: Database: ${options.url}. Cannot execute query on finalized transaction` + `OP-Sqlite Error: Database: ${ + options.name || options.url + }. Cannot execute query on finalized transaction` ); } const result = await enhancedDb.execute('ROLLBACK;'); @@ -253,7 +534,6 @@ function enhanceDB(db: DB, options: any): DB { await commit(); } } catch (executionError) { - // console.warn('transaction error', executionError); if (!isFinalized) { try { await rollback(); @@ -286,10 +566,11 @@ function enhanceDB(db: DB, options: any): DB { return enhancedDb; } -/** Open a replicating connection via libsql to a turso db +/** + * Open a replicating connection via libsql to a turso db * libsql needs to be enabled on your package.json */ -export const openSync = (options: { +export const openSync = (params: { url: string; authToken: string; name: string; @@ -300,44 +581,56 @@ export const openSync = (options: { throw new Error('This function is only available for libsql'); } - const db = OPSQLite.openSync(options); - const enhancedDb = enhanceDB(db, options); + const db = OPSQLite.openSync(params); + const enhancedDb = enhanceDB(db, params); return enhancedDb; }; -/** Open a remote connection via libsql to a turso db +/** + * Open a remote connection via libsql to a turso db * libsql needs to be enabled on your package.json */ -export const openRemote = (options: { url: string; authToken: string }): DB => { +export const openRemote = (params: { url: string; authToken: string }): DB => { if (!isLibsql()) { throw new Error('This function is only available for libsql'); } - const db = OPSQLite.openRemote(options); - const enhancedDb = enhanceDB(db, options); + const db = OPSQLite.openRemote(params); + const enhancedDb = enhanceDB(db, params); return enhancedDb; }; -export const open = (options: { +/** + * Open a connection to a local sqlite or sqlcipher database + * If you want libsql remote or sync connections, use openSync or openRemote + */ +export const open = (params: { name: string; location?: string; encryptionKey?: string; }): DB => { - if (options.location?.startsWith('file://')) { + if (params.location?.startsWith('file://')) { console.warn( "[op-sqlite] You are passing a path with 'file://' prefix, it's automatically removed" ); - options.location = options.location.substring(7); + params.location = params.location.substring(7); } - const db = OPSQLite.open(options); - const enhancedDb = enhanceDB(db, options); + const db = OPSQLite.open(params); + const enhancedDb = enhanceDB(db, params); return enhancedDb; }; +/** + * Moves the database from the assets folder to the default path (check the docs) or to a custom path + * It DOES NOT OVERWRITE the database if it already exists in the destination path + * if you want to overwrite the database, you need to pass the overwrite flag as true + * @param args object with the parameters for the operaiton + * @returns promise, rejects if failed to move the database, resolves if the operation was successful + */ export const moveAssetsDatabase = async (args: { filename: string; path?: string; @@ -346,6 +639,14 @@ export const moveAssetsDatabase = async (args: { return NativeModules.OPSQLite.moveAssetsDatabase(args); }; +/** + * Used to load a dylib file that contains a sqlite 3 extension/plugin + * It returns the raw path to the actual file which then needs to be passed to the loadExtension function + * Check the docs for more information + * @param bundle the iOS bundle identifier of the .framework + * @param name the file name of the dylib file + * @returns + */ export const getDylibPath = (bundle: string, name: string): string => { return NativeModules.OPSQLite.getDylibPath(bundle, name); }; diff --git a/src/types.d.ts b/src/types.d.ts deleted file mode 100644 index 3adb2dd..0000000 --- a/src/types.d.ts +++ /dev/null @@ -1,164 +0,0 @@ -type Scalar = string | number | boolean | null | ArrayBuffer | ArrayBufferView; - -/** - * Object returned by SQL Query executions { - * insertId: Represent the auto-generated row id if applicable - * rowsAffected: Number of affected rows if result of a update query - * message: if status === 1, here you will find error description - * rows: if status is undefined or 0 this object will contain the query results - * } - * - * @interface QueryResult - */ -type QueryResult = { - insertId?: number; - rowsAffected: number; - res?: any[]; - rows: Array<Record<string, Scalar>>; - // An array of intermediate results, just values without column names - rawRows?: Scalar[][]; - columnNames?: string[]; - /** - * Query metadata, available only for select query results - */ - metadata?: ColumnMetadata[]; -}; - -/** - * Column metadata - * Describes some information about columns fetched by the query - */ -type ColumnMetadata = { - /** The name used for this column for this result set */ - name: string; - /** The declared column type for this column, when fetched directly from a table or a View resulting from a table column. "UNKNOWN" for dynamic values, like function returned ones. */ - type: string; - /** - * The index for this column for this result set*/ - index: number; -}; - -/** - * Allows the execution of bulk of sql commands - * inside a transaction - * If a single query must be executed many times with different arguments, its preferred - * to declare it a single time, and use an array of array parameters. - */ -type SQLBatchTuple = [string] | [string, Array<any> | Array<Array<any>>]; - -type UpdateHookOperation = 'INSERT' | 'DELETE' | 'UPDATE'; - -/** - * status: 0 or undefined for correct execution, 1 for error - * message: if status === 1, here you will find error description - * rowsAffected: Number of affected rows if status == 0 - */ -type BatchQueryResult = { - rowsAffected?: number; -}; - -/** - * Result of loading a file and executing every line as a SQL command - * Similar to BatchQueryResult - */ -type FileLoadResult = BatchQueryResult & { - commands?: number; -}; - -type Transaction = { - commit: () => Promise<QueryResult>; - execute: (query: string, params?: Scalar[]) => Promise<QueryResult>; - rollback: () => Promise<QueryResult>; -}; - -type PendingTransaction = { - /* - * The start function should not throw or return a promise because the - * queue just calls it and does not monitor for failures or completions. - * - * It should catch any errors and call the resolve or reject of the wrapping - * promise when complete. - * - * It should also automatically commit or rollback the transaction if needed - */ - start: () => void; -}; - -type PreparedStatement = { - bind: (params: any[]) => Promise<void>; - execute: () => Promise<QueryResult>; -}; - -type DB = { - close: () => void; - delete: (location?: string) => void; - attach: ( - mainDbName: string, - dbNameToAttach: string, - alias: string, - location?: string - ) => void; - detach: (mainDbName: string, alias: string) => void; - transaction: (fn: (tx: Transaction) => Promise<void>) => Promise<void>; - executeSync: (query: string, params?: Scalar[]) => QueryResult; - execute: (query: string, params?: Scalar[]) => Promise<QueryResult>; - executeWithHostObjects: ( - query: string, - params?: Scalar[] - ) => Promise<QueryResult>; - executeBatch: (commands: SQLBatchTuple[]) => Promise<BatchQueryResult>; - loadFile: (location: string) => Promise<FileLoadResult>; - updateHook: ( - callback?: - | ((params: { - table: string; - operation: UpdateHookOperation; - row?: any; - rowId: number; - }) => void) - | null - ) => void; - commitHook: (callback?: (() => void) | null) => void; - rollbackHook: (callback?: (() => void) | null) => void; - prepareStatement: (query: string) => PreparedStatement; - loadExtension: (path: string, entryPoint?: string) => void; - executeRaw: (query: string, params?: Scalar[]) => Promise<any[]>; - getDbPath: (location?: string) => string; - reactiveExecute: (params: { - query: string; - arguments: any[]; - fireOn: { - table: string; - ids?: number[]; - }[]; - callback: (response: any) => void; - }) => () => void; - /** This function is only available for libsql. - * Allows to trigger a sync the database with it's remote replica - * In order for this function to work you need to use openSync or openRemote functions - * with libsql: true in the package.json - * - * The database is hosted in turso - **/ - sync: () => void; - flushPendingReactiveQueries: () => Promise<void>; -}; - -type OPSQLiteProxy = { - open: (options: { - name: string; - location?: string; - encryptionKey?: string; - }) => DB; - openRemote: (options: { url: string; authToken: string }) => DB; - openSync: (options: { - url: string; - authToken: string; - name: string; - location?: string; - syncInterval?: number; - }) => DB; - isSQLCipher: () => boolean; - isLibsql: () => boolean; - isIOSEmbedded: () => boolean; -};