Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ name: Node.js Package
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]

jobs:
build:
Expand All @@ -17,8 +15,8 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 16
- run: yarn
- run: yarn test
- run: npm ci
- run: npm test

publish-npm:
needs: build
Expand All @@ -29,8 +27,8 @@ jobs:
with:
node-version: 14
registry-url: https://registry.npmjs.org/
- run: yarn
- run: yarn build
- run: npm ci
- run: npm run build
- run: npm publish --registry=https://npm.pkg.github.com/
env:
GITHUB_TOKEN: ${{secrets.NPM_TOKEN}}
73 changes: 73 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.0.0] - 2025-02-27

### Breaking Changes

- **Migrated to Expo SQLite new API**: This version requires Expo SDK 51+ and uses the new `expo-sqlite` API. The legacy `expo-sqlite/legacy` API is no longer supported.
- **Peer dependency updated**: `expo-sqlite` peer dependency changed from `^14.0.3` to `^14.0.0 || ^15.0.0`
- **Database API changes**:
- `Database.transaction()` method removed (replaced with internal `withTransactionAsync()`)
- Database operations are now fully async-first

### Added

- Support for Expo SDK 52+ with new SQLite API
- `Database.withTransactionAsync()` method for executing custom transactions
- Improved type safety for `DatabaseLayer.executeBulkSql()` parameters
- Enhanced error handling in transaction callbacks
- Comprehensive test coverage for transaction error cases

### Changed

- **Database.ts**:
- Migrated from `expo-sqlite/legacy` to `expo-sqlite`
- `openDatabase()` → `openDatabaseAsync()` (async)
- `transaction(callback)` → `withTransactionAsync()` (async)
- `deleteAsync()` → `SQLite.deleteDatabaseAsync()` (static method)
- All database operations now use async/await pattern
- **DatabaseLayer.ts**:
- Updated to use `runAsync()` for INSERT/UPDATE/DELETE operations
- Updated to use `getAllAsync()` for SELECT queries
- Improved type safety for bulk SQL operations
- **Tests**:
- Updated all mocks to use new expo-sqlite API
- Added tests for `Database.reset()` functionality
- Added tests for transaction error handling
- Improved test coverage to ~90%

### Fixed

- Fixed unsafe non-null assertion in `withTransactionAsync()` that could cause runtime errors
- Improved error handling when transaction callbacks don't return values
- Fixed type safety issues in `executeBulkSql()` parameter handling

### Security

- All SQL queries continue to use parameterized statements to prevent SQL injection
- No security vulnerabilities introduced

### Migration Guide

If you're upgrading from version 2.x:

1. **Update Expo SDK**: Ensure you're using Expo SDK 51 or higher
2. **Update expo-sqlite**: Run `npx expo install expo-sqlite` to get the compatible version
3. **No code changes required**: The public API remains the same - only internal implementation changed
4. **Breaking change**: If you were directly using `Database.transaction()`, you'll need to use `Database.withTransactionAsync()` instead

### Technical Details

- Database connections are now lazy-loaded and cached using promises
- Transactions use `withTransactionAsync()` for atomic operations
- SQL statement detection improved for SELECT vs write operations
- All database operations properly handle async errors

## [2.3.1] - Previous Version

Previous version using Expo SDK 50 and legacy SQLite API.
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,25 @@
<a href="https://www.npmjs.com/package/expo-sqlite-orm"><img src="https://img.shields.io/npm/v/expo-sqlite-orm.svg" alt="Version"></a>
<a href="https://www.npmjs.com/package/expo-sqlite-orm"><img src="https://img.shields.io/npm/l/expo-sqlite-orm.svg" alt="License"></a>

> **Note**: This is a fork of [expo-sqlite-orm](https://github.com/dflourusso/expo-sqlite-orm) by [Daniel Fernando Lourusso](http://dflourusso.com.br). This fork adds support for Expo SDK 52+ by migrating to the new `expo-sqlite` API that replaced the legacy implementation.
>
> **Why this fork?** The original package uses the legacy `expo-sqlite/legacy` API which was removed in Expo SDK 52. This fork maintains the same public API while using the new async-first SQLite API under the hood, ensuring compatibility with modern Expo versions.
>
> For details on what changed, see the [CHANGELOG.md](./CHANGELOG.md).

It is a simple ORM utility to use with expo sqlite

> Warn: it works only on iOS and Android. Web is not supported ([SEE](https://docs.expo.io/versions/latest/sdk/sqlite/))

## Install

`yarn add expo-sqlite-orm`
```bash
npm install @esign/expo-sqlite-orm
# or
yarn add @esign/expo-sqlite-orm
```

> **Note**: This package requires Expo SDK 51+ and `expo-sqlite` version 14.0.0 or higher. See [CHANGELOG.md](./CHANGELOG.md) for migration details.

## Basic usage

Expand All @@ -24,7 +36,7 @@ You need to provide 3 things:

```typescript
import { Text } from '@components'
import { ColumnMapping, columnTypes, IStatement, Migrations, Repository, sql } from 'expo-sqlite-orm'
import { ColumnMapping, columnTypes, IStatement, Migrations, Repository, sql } from '@esign/expo-sqlite-orm'
import React, { useMemo, useState } from 'react'
import { ScrollView } from 'react-native'

Expand Down Expand Up @@ -235,7 +247,7 @@ animalRepository.databaseLayer.bulkInsertOrReplace(itens).then(response => {

```typescript
import * as SQLite from 'expo-sqlite'
import { Migrations, sql } from 'expo-sqlite-orm'
import { Migrations, sql } from '@esign/expo-sqlite-orm'

const statements: IStatement = {
'1662689376195_init': sql`CREATE TABLE animals (id TEXT, name TEXT);`,
Expand Down Expand Up @@ -266,6 +278,10 @@ await migrations.reset()

## Changelog

For a complete list of changes, see [CHANGELOG.md](./CHANGELOG.md).

### Previous versions (from original package)

- **1.5.0** - Return unlimited rows if `page` is not specified in the `query` params
- **1.6.0** - Make `autoincrement` property to be optional
- **2.0.0** - BREAKING CHANGE
Expand Down Expand Up @@ -295,10 +311,16 @@ docker-compose run --rm app test
- [https://github.com/dflourusso/expo-sqlite-orm-example](https://github.com/dflourusso/expo-sqlite-orm-example)
- [https://snack.expo.io/@dflourusso/expo-sqlite-orm-example](https://snack.expo.io/@dflourusso/expo-sqlite-orm-example)

## Author
## Credits

This package is a fork of [expo-sqlite-orm](https://github.com/dflourusso/expo-sqlite-orm) by [Daniel Fernando Lourusso](http://dflourusso.com.br).

### Original Author
- [Daniel Fernando Lourusso](http://dflourusso.com.br)

### Fork Maintainer
- eSign Team

## License

This project is licensed under
Expand Down
27 changes: 14 additions & 13 deletions __mocks__/expo-sqlite/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
export const openDatabase = jest.fn(() => ({
closeAsync: jest.fn(),
deleteAsync: jest.fn(),
transaction: jest.fn((cb) => cb({
executeSql: jest.fn((sql, params, onSuccess, onError) => {
if (sql === '') {
onSuccess(null, { rows: { _array: [] }, insertId: null })
return
}
onSuccess(null, { rows: { _array: [] }, insertId: params[0] || 1 })
})
}))
}))
export const mockDb = {
getAllAsync: jest.fn(async () => []),
runAsync: jest.fn(async (sql: string) => ({
lastInsertRowId: /^INSERT/i.test(sql) ? 1 : null,
changes: 1
})),
closeAsync: jest.fn(async () => {})
}
mockDb.withTransactionAsync = jest.fn(async (fn: (db: typeof mockDb) => Promise<any>) =>
fn(mockDb)
)

export const openDatabaseAsync = jest.fn(async () => mockDb)
export const deleteDatabaseAsync = jest.fn(async () => {})
57 changes: 57 additions & 0 deletions __tests__/Database.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { deleteDatabaseAsync, mockDb } from 'expo-sqlite'
import { Database } from '../src/Database'

jest.mock('expo-sqlite')

describe('Database', () => {
beforeEach(() => {
jest.clearAllMocks()
mockDb.getAllAsync.mockResolvedValue([])
mockDb.runAsync.mockResolvedValue({ lastInsertRowId: 1, changes: 1 })
})

it('reset closes database and deletes file', async () => {
const db = Database.instance('reset-test')
await db.runSql('SELECT 1')
await db.reset()
expect(mockDb.closeAsync).toHaveBeenCalled()
expect(deleteDatabaseAsync).toHaveBeenCalledWith('reset-test')
})

describe('withTransactionAsync', () => {
it('executes callback within transaction and returns result', async () => {
const db = Database.instance('transaction-test')
const result = await db.withTransactionAsync(async (txnDb) => {
await txnDb.runAsync('INSERT INTO test VALUES (?)', ['value'])
return { success: true }
})
expect(result).toEqual({ success: true })
expect(mockDb.withTransactionAsync).toHaveBeenCalledTimes(1)
})

it('throws error if callback does not return a value', async () => {
const db = Database.instance('transaction-error-test')
mockDb.withTransactionAsync.mockImplementationOnce(async (fn) => {
await fn(mockDb)
})
await expect(
db.withTransactionAsync(async () => {
await mockDb.runAsync('SELECT 1')
})
).rejects.toThrow('Transaction callback did not return a value')
})

it('propagates errors from transaction callback', async () => {
const db = Database.instance('transaction-error-test')
const error = new Error('Transaction failed')
mockDb.withTransactionAsync.mockImplementationOnce(async (fn) => {
throw error
})
await expect(
db.withTransactionAsync(async () => {
return { success: true }
})
).rejects.toThrow('Transaction failed')
})
})
})
39 changes: 18 additions & 21 deletions __tests__/DatabaseLayer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,45 +18,38 @@ jest.mock('../src/query_builder', () => {
}, {})
})

import { openDatabase } from "expo-sqlite/legacy"
import { Database } from '../src/Database'
import { mockDb } from 'expo-sqlite'
import { DatabaseLayer } from '../src/DatabaseLayer'
import Qb from '../src/query_builder'
import { IQueryOptions } from '../src/types'

jest.mock('expo-sqlite/legacy')

interface ITests {
id: number
teste1: string
teste2: number
teste3: string
}


const databaseName = 'databaseName'
const executeSql = jest.fn((sql, params, cb, errorCb) => {
const insertId = /^INSERT/.test(sql) ? 1 : null
cb(null, { rows: { _array: [] }, insertId })
})
const transaction = jest.fn(cb => cb({ executeSql }))
const database = { transaction } as unknown as Database
const tableName = 'tests'

describe('execute sql', () => {
const databaseLayer = new DatabaseLayer<ITests>(databaseName, tableName)
beforeEach(() => {
jest.clearAllMocks()
mockDb.getAllAsync.mockResolvedValue([])
mockDb.runAsync.mockImplementation(async (sql: string) => ({
lastInsertRowId: /^INSERT/i.test(sql) ? 1 : null,
changes: 1
}))
})

it('call execute with the correct params', () => {
(openDatabase as jest.Mock).mockImplementationOnce(() => database)
const sql = 'select * from tests where id = ?'
const params = [1]
return databaseLayer.executeSql(sql, params).then(() => {
expect(executeSql).toHaveBeenCalledTimes(1)
expect(executeSql).toHaveBeenCalledWith(
sql,
params,
expect.any(Function),
expect.any(Function)
)
expect(mockDb.getAllAsync).toHaveBeenCalledTimes(1)
expect(mockDb.getAllAsync).toHaveBeenCalledWith(sql, params)
})
})

Expand Down Expand Up @@ -104,12 +97,16 @@ describe('run statements', () => {
})

it('update', () => {
const updateFn = jest.fn(() => Promise.resolve())
const updatedRow = { id: 1, teste1: 'teste', teste2: 2, teste3: '{"prop":123}' }
const updateFn = jest.fn()
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce({ rows: [updatedRow], insertId: null })
databaseLayer.executeSql = updateFn
const resource = { id: 1, teste1: 'teste', teste2: 2, teste3: '{"prop":123}' }
return databaseLayer.update(resource).then(() => {
return databaseLayer.update(resource).then((res) => {
expect(Qb.update).toBeCalledWith(tableName, resource)
expect(updateFn).toBeCalledWith(qbMockReturns, ['teste', 2, '{"prop":123}', 1])
expect(res).toEqual(updatedRow)
})
})

Expand Down
17 changes: 7 additions & 10 deletions __tests__/Migrations.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
jest.mock('../src/DatabaseLayer')
import { openDatabase, SQLiteDatabase } from 'expo-sqlite/legacy'
const mockReset = jest.fn()
jest.mock('../src/Database', () => ({
Database: {
instance: jest.fn(() => ({ reset: mockReset }))
}
}))
import { IStatement, Migrations, sql } from '../src/Migrations'


const databasenName = 'databaseName'

const databaseInstance = {
closeAsync: jest.fn(),
deleteAsync: jest.fn()
} as unknown as SQLiteDatabase

const statements: IStatement = {
'1662689376195_init': sql`CREATE TABLE animals (id TEXT, name TEXT);`,
'1662689376197_add_color_column': sql`ALTER TABLE animals ADD color TEXT;`,
Expand All @@ -19,7 +18,6 @@ const statements: IStatement = {
describe('Migrations', () => {
let migrations: Migrations
beforeEach(() => {
(openDatabase as jest.Mock).mockImplementationOnce(() => databaseInstance)
migrations = new Migrations(databasenName, statements)
jest.clearAllMocks()
})
Expand Down Expand Up @@ -69,7 +67,6 @@ describe('Migrations', () => {

it('Should reset the database', async () => {
await migrations.reset()
expect(databaseInstance.closeAsync).toHaveBeenCalled()
expect(databaseInstance.deleteAsync).toHaveBeenCalled()
expect(mockReset).toHaveBeenCalled()
})
})
Loading