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
25 changes: 25 additions & 0 deletions packages/dialects/sql.js/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
Forked from https://github.com/betarixm/kysely-sql-js

## Usage

```ts
import { type GeneratedAlways, Kysely } from 'kysely';
import initSqlJs from 'sql.js';

import { SqlJsDialect } from 'kysely-sql-js';

interface Database {
person: {
id: GeneratedAlways<number>;
first_name: string | null;
last_name: string | null;
age: number;
};
}

const SqlJsStatic = await initSqlJs();

export const db = new Kysely<Database>({
dialect: new SqlJsDialect({ sqlJs: new SqlJsStatic.Database() }),
});
```
4 changes: 4 additions & 0 deletions packages/dialects/sql.js/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import config from '@zenstackhq/eslint-config/base.js';

/** @type {import("eslint").Linter.Config} */
export default config;
42 changes: 42 additions & 0 deletions packages/dialects/sql.js/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "@zenstackhq/kysely-sql-js",
"version": "3.0.0-alpha.16",
"description": "Kysely dialect for sql.js",
"type": "module",
"scripts": {
"build": "tsup-node",
"watch": "tsup-node --watch",
"lint": "eslint src --ext ts",
"pack": "pnpm pack"
},
"keywords": [],
"author": "ZenStack Team",
"license": "MIT",
"files": [
"dist"
],
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
},
"devDependencies": {
"@types/sql.js": "^1.4.9",
"@zenstackhq/eslint-config": "workspace:*",
"@zenstackhq/typescript-config": "workspace:*",
"@zenstackhq/vitest-config": "workspace:*",
"sql.js": "^1.13.0",
"kysely": "catalog:"
},
"peerDependencies": {
"sql.js": "^1.13.0",
"kysely": "catalog:"
}
}
30 changes: 30 additions & 0 deletions packages/dialects/sql.js/src/connection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { DatabaseConnection, QueryResult } from 'kysely';
import type { BindParams, Database } from 'sql.js';

import { CompiledQuery } from 'kysely';

export class SqlJsConnection implements DatabaseConnection {
private database: Database;

constructor(database: Database) {
this.database = database;
}

async executeQuery<R>(compiledQuery: CompiledQuery<unknown>): Promise<QueryResult<R>> {
const executeResult = this.database.exec(compiledQuery.sql, compiledQuery.parameters as BindParams);
const rowsModified = this.database.getRowsModified();
return {
numAffectedRows: BigInt(rowsModified),
rows: executeResult
.map(({ columns, values }) =>
values.map((row) => columns.reduce((acc, column, i) => ({ ...acc, [column]: row[i] }), {}) as R),
)
.flat(),
};
}

// eslint-disable-next-line require-yield
async *streamQuery() {
throw new Error('Not supported with SQLite');
}
}
26 changes: 26 additions & 0 deletions packages/dialects/sql.js/src/dialect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Dialect } from 'kysely';

import type { SqlJsDialectConfig } from './types';

import { Kysely, SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from 'kysely';

import { SqlJsDriver } from './driver';

/**
* The SqlJsDialect is for testing purposes only and should not be used in production.
*/
export class SqlJsDialect implements Dialect {
private config: SqlJsDialectConfig;

constructor(config: SqlJsDialectConfig) {
this.config = config;
}

createAdapter = () => new SqliteAdapter();

createDriver = () => new SqlJsDriver(this.config);

createIntrospector = (db: Kysely<any>) => new SqliteIntrospector(db);

createQueryCompiler = () => new SqliteQueryCompiler();
}
38 changes: 38 additions & 0 deletions packages/dialects/sql.js/src/driver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { DatabaseConnection, Driver } from 'kysely';

import { CompiledQuery } from 'kysely';

import { SqlJsConnection } from './connection';
import type { SqlJsDialectConfig } from './types';

export class SqlJsDriver implements Driver {
private config: SqlJsDialectConfig;

constructor(config: SqlJsDialectConfig) {
this.config = config;
}

async acquireConnection(): Promise<DatabaseConnection> {
return new SqlJsConnection(this.config.sqlJs);
}

async beginTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw('BEGIN'));
}

async commitTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw('COMMIT'));
}

async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw('ROLLBACK'));
}

async destroy(): Promise<void> {
this.config.sqlJs.close();
}

async init() {}

async releaseConnection(_connection: DatabaseConnection): Promise<void> {}
}
4 changes: 4 additions & 0 deletions packages/dialects/sql.js/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './connection';
export * from './dialect';
export * from './driver';
export * from './types';
5 changes: 5 additions & 0 deletions packages/dialects/sql.js/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { Database } from 'sql.js';

export interface SqlJsDialectConfig {
sqlJs: Database;
}
12 changes: 12 additions & 0 deletions packages/dialects/sql.js/test/getting-started/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Database } from './types';

import { Kysely } from 'kysely';
import initSqlJs from 'sql.js';

import { SqlJsDialect } from '../../src';

const SqlJsStatic = await initSqlJs();

export const db = new Kysely<Database>({
dialect: new SqlJsDialect({ sqlJs: new SqlJsStatic.Database() }),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { sql } from 'kysely';
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { db } from './database';
import * as PersonRepository from './person-repository';

describe('person-repository', () => {
beforeEach(async () => {
await db
.insertInto('person')
.values({
id: 123,
first_name: 'Arnold',
last_name: 'Schwarzenegger',
gender: 'other',
})
.executeTakeFirstOrThrow();
});

beforeAll(async () => {
await db.schema
.createTable('person')
.addColumn('id', 'integer', (cb) => cb.primaryKey().autoIncrement().notNull())
.addColumn('first_name', 'varchar(255)', (cb) => cb.notNull())
.addColumn('last_name', 'varchar(255)')
.addColumn('gender', 'varchar(50)', (cb) => cb.notNull())
.addColumn('created_at', 'timestamp', (cb) => cb.notNull().defaultTo(sql`current_timestamp`))
.execute();
});

afterEach(async () => {
await sql`delete from ${sql.table('person')}`.execute(db);
});

afterAll(async () => {
await db.schema.dropTable('person').execute();
});

it('should find a person with a given id', async () => {
expect(await PersonRepository.findPersonById(123)).toMatchObject({
id: 123,
first_name: 'Arnold',
last_name: 'Schwarzenegger',
gender: 'other',
});
});

it('should find all people named Arnold', async () => {
const people = await PersonRepository.findPeople({ first_name: 'Arnold' });

expect(people).toHaveLength(1);
expect(people[0]).toMatchObject({
id: 123,
first_name: 'Arnold',
last_name: 'Schwarzenegger',
gender: 'other',
});
});

it('should update gender of a person with a given id', async () => {
await PersonRepository.updatePerson(123, { gender: 'woman' });

expect(await PersonRepository.findPersonById(123)).toMatchObject({
id: 123,
first_name: 'Arnold',
last_name: 'Schwarzenegger',
gender: 'woman',
});
});

it('should create a person', async () => {
await PersonRepository.createPerson({
first_name: 'Jennifer',
last_name: 'Aniston',
gender: 'woman',
});

expect(await PersonRepository.findPeople({ first_name: 'Jennifer' })).toHaveLength(1);
});

it('should create multiple persons', async () => {
const created = await PersonRepository.createPersons([
{ first_name: 'Brad', last_name: 'Pitt', gender: 'man' },
{ first_name: 'Angelina', last_name: 'Jolie', gender: 'woman' },
]);
await expect(PersonRepository.findPeople({ first_name: 'Brad' })).resolves.toBeTruthy();
await expect(PersonRepository.findPeople({ first_name: 'Angelina' })).resolves.toBeTruthy();
});

it('should delete a person with a given id', async () => {
await PersonRepository.deletePerson(123);

expect(await PersonRepository.findPersonById(123)).toBeUndefined();
});
});
48 changes: 48 additions & 0 deletions packages/dialects/sql.js/test/getting-started/person-repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { PersonUpdate, Person, NewPerson } from './types';

import { db } from './database';

export async function findPersonById(id: number) {
return await db.selectFrom('person').where('id', '=', id).selectAll().executeTakeFirst();
}

export async function findPeople(criteria: Partial<Person>) {
let query = db.selectFrom('person');

if (criteria.id) {
query = query.where('id', '=', criteria.id); // Kysely is immutable, you must re-assign!
}

if (criteria.first_name) {
query = query.where('first_name', '=', criteria.first_name);
}

if (criteria.last_name !== undefined) {
query = query.where('last_name', criteria.last_name === null ? 'is' : '=', criteria.last_name);
}

if (criteria.gender) {
query = query.where('gender', '=', criteria.gender);
}

if (criteria.created_at) {
query = query.where('created_at', '=', criteria.created_at);
}

return await query.selectAll().execute();
}

export async function updatePerson(id: number, updateWith: PersonUpdate) {
await db.updateTable('person').set(updateWith).where('id', '=', id).execute();
}

export async function createPerson(person: NewPerson) {
return await db.insertInto('person').values(person).returningAll().executeTakeFirstOrThrow();
}

export async function createPersons(persons: NewPerson[]) {
return await db.insertInto('person').values(persons).executeTakeFirstOrThrow();
}
export async function deletePerson(id: number) {
return await db.deleteFrom('person').where('id', '=', id).returningAll().executeTakeFirst();
}
53 changes: 53 additions & 0 deletions packages/dialects/sql.js/test/getting-started/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { ColumnType, Generated, Insertable, Selectable, Updateable } from 'kysely';

export interface Database {
person: PersonTable;
pet: PetTable;
}

// This interface describes the `person` table to Kysely. Table
// interfaces should only be used in the `Database` type above
// and never as a result type of a query!. See the `Person`,
// `NewPerson` and `PersonUpdate` types below.
export interface PersonTable {
// Columns that are generated by the database should be marked
// using the `Generated` type. This way they are automatically
// made optional in inserts and updates.
id: Generated<number>;

first_name: string;
gender: 'man' | 'woman' | 'other';

// If the column is nullable in the database, make its type nullable.
// Don't use optional properties. Optionality is always determined
// automatically by Kysely.
last_name: string | null;

// You can specify a different type for each operation (select, insert and
// update) using the `ColumnType<SelectType, InsertType, UpdateType>`
// wrapper. Here we define a column `created_at` that is selected as
// a `Date`, can optionally be provided as a `string` in inserts and
// can never be updated:
created_at: ColumnType<Date, string | undefined, never>;
}

// You should not use the table schema interfaces directly. Instead, you should
// use the `Selectable`, `Insertable` and `Updateable` wrappers. These wrappers
// make sure that the correct types are used in each operation.
//
// Most of the time you should trust the type inference and not use explicit
// types at all. These types can be useful when typing function arguments.
export type Person = Selectable<PersonTable>;
export type NewPerson = Insertable<PersonTable>;
export type PersonUpdate = Updateable<PersonTable>;

export interface PetTable {
id: Generated<number>;
name: string;
owner_id: number;
species: 'dog' | 'cat';
}

export type Pet = Selectable<PetTable>;
export type NewPet = Insertable<PetTable>;
export type PetUpdate = Updateable<PetTable>;
Loading