From 7b4d0c888f2c5f17d41e99cfbb2ae6b264a8109a Mon Sep 17 00:00:00 2001 From: Julien Ripouteau Date: Mon, 11 Mar 2024 01:40:49 +0100 Subject: [PATCH 1/9] feat: add kysely adapter --- README.md | 1 + packages/verrou/package.json | 4 + .../src/drivers/{database.ts => knex.ts} | 36 +--- packages/verrou/src/drivers/kysely.ts | 164 ++++++++++++++++++ packages/verrou/src/types/drivers.ts | 51 ++++-- packages/verrou/test_helpers/index.ts | 25 +-- packages/verrou/tests/drivers/knex/helpers.ts | 18 ++ .../{database.spec.ts => knex/knex.spec.ts} | 12 +- .../verrou/tests/drivers/knex/mysql.spec.ts | 18 ++ .../tests/drivers/knex/postgres.spec.ts | 18 ++ .../tests/drivers/{ => knex}/sqlite.spec.ts | 10 +- .../verrou/tests/drivers/kysely/helpers.ts | 15 ++ .../verrou/tests/drivers/kysely/mysql.spec.ts | 21 +++ .../tests/drivers/kysely/postgres.spec.ts | 21 +++ .../tests/drivers/kysely/sqlite.spec.ts | 22 +++ packages/verrou/tests/drivers/mysql.spec.ts | 20 --- .../verrou/tests/drivers/postgres.spec.ts | 20 --- pnpm-lock.yaml | 92 +++++++++- 18 files changed, 459 insertions(+), 109 deletions(-) rename packages/verrou/src/drivers/{database.ts => knex.ts} (77%) create mode 100644 packages/verrou/src/drivers/kysely.ts create mode 100644 packages/verrou/tests/drivers/knex/helpers.ts rename packages/verrou/tests/drivers/{database.spec.ts => knex/knex.spec.ts} (72%) create mode 100644 packages/verrou/tests/drivers/knex/mysql.spec.ts create mode 100644 packages/verrou/tests/drivers/knex/postgres.spec.ts rename packages/verrou/tests/drivers/{ => knex}/sqlite.spec.ts (54%) create mode 100644 packages/verrou/tests/drivers/kysely/helpers.ts create mode 100644 packages/verrou/tests/drivers/kysely/mysql.spec.ts create mode 100644 packages/verrou/tests/drivers/kysely/postgres.spec.ts create mode 100644 packages/verrou/tests/drivers/kysely/sqlite.spec.ts delete mode 100644 packages/verrou/tests/drivers/mysql.spec.ts delete mode 100644 packages/verrou/tests/drivers/postgres.spec.ts diff --git a/README.md b/README.md index 6e7ebf5..e6458bd 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ - ๐Ÿ”’ Easy usage - ๐Ÿ”„ Multiple drivers (Redis, Postgres, MySQL, Sqlite, In-Memory and others) +- ๐Ÿ“ฆ Multiple database adapters ( Knex, Kysely, Drizzle ...) - ๐Ÿ”‘ Customizable named locks - ๐ŸŒ Consistent API across all drivers - ๐Ÿงช Easy testing by switching to an in-memory driver diff --git a/packages/verrou/package.json b/packages/verrou/package.json index 82d284a..909fc8a 100644 --- a/packages/verrou/package.json +++ b/packages/verrou/package.json @@ -45,9 +45,13 @@ }, "devDependencies": { "@aws-sdk/client-dynamodb": "^3.529.1", + "@types/better-sqlite3": "^7.6.9", + "@types/pg": "^8.11.2", "@types/proper-lockfile": "^4.1.4", + "better-sqlite3": "^9.4.3", "ioredis": "^5.3.2", "knex": "^3.1.0", + "kysely": "^0.27.3", "mysql2": "^3.9.2", "pg": "^8.11.3", "sqlite3": "^5.1.7" diff --git a/packages/verrou/src/drivers/database.ts b/packages/verrou/src/drivers/knex.ts similarity index 77% rename from packages/verrou/src/drivers/database.ts rename to packages/verrou/src/drivers/knex.ts index ea69fa2..4f7f176 100644 --- a/packages/verrou/src/drivers/database.ts +++ b/packages/verrou/src/drivers/knex.ts @@ -1,16 +1,16 @@ -import knex, { type Knex } from 'knex' +import type { Knex } from 'knex' import { E_LOCK_NOT_OWNED } from '../errors.js' -import type { DatabaseStoreOptions, LockStore } from '../types/main.js' +import type { KnexStoreOptions, LockStore } from '../types/main.js' /** * Create a new database store */ -export function databaseStore(config: DatabaseStoreOptions) { - return { config, factory: () => new DatabaseStore(config) } +export function knexStore(config: KnexStoreOptions) { + return { config, factory: () => new KnexStore(config) } } -export class DatabaseStore implements LockStore { +export class KnexStore implements LockStore { /** * Knex connection instance */ @@ -26,8 +26,8 @@ export class DatabaseStore implements LockStore { */ #initialized: Promise - constructor(config: DatabaseStoreOptions) { - this.#connection = this.#createConnection(config) + constructor(config: KnexStoreOptions) { + this.#connection = config.connection this.#tableName = config.tableName || this.#tableName if (config.autoCreateTable !== false) { this.#initialized = this.#createTableIfNotExists() @@ -37,27 +37,7 @@ export class DatabaseStore implements LockStore { } /** - * Create or reuse a Knex connection instance - */ - #createConnection(config: DatabaseStoreOptions) { - if (typeof config.connection === 'string') { - return knex({ client: config.dialect, connection: config.connection, useNullAsDefault: true }) - } - - /** - * This looks hacky. Maybe we can find a better way to do this? - * We check if config.connection is a Knex object. If it is, we - * return it as is. If it's not, we create a new Knex object - */ - if ('with' in config.connection!) { - return config.connection - } - - return knex({ client: config.dialect, connection: config.connection, useNullAsDefault: true }) - } - - /** - * Create the cache table if it doesn't exist + * Create the locks table if it doesn't exist */ async #createTableIfNotExists() { const hasTable = await this.#connection.schema.hasTable(this.#tableName) diff --git a/packages/verrou/src/drivers/kysely.ts b/packages/verrou/src/drivers/kysely.ts new file mode 100644 index 0000000..81c7d10 --- /dev/null +++ b/packages/verrou/src/drivers/kysely.ts @@ -0,0 +1,164 @@ +import type { Kysely } from 'kysely' + +import { E_LOCK_NOT_OWNED } from '../errors.js' +import type { KyselyOptions, LockStore } from '../types/main.js' + +/** + * Create a new knex store + */ +export function knexStore(config: KyselyOptions) { + return { config, factory: () => new KyselyStore(config) } +} + +export class KyselyStore implements LockStore { + /** + * Knex connection instance + */ + #connection: Kysely + + /** + * The name of the table used to store locks + */ + #tableName = 'verrou' + + /** + * A promise that resolves when the table is created + */ + #initialized: Promise + + constructor(config: KyselyOptions) { + this.#connection = config.connection + this.#tableName = config.tableName || this.#tableName + if (config.autoCreateTable !== false) { + this.#initialized = this.#createTableIfNotExists() + } else { + this.#initialized = Promise.resolve() + } + } + + /** + * Create the locks table if it doesn't exist + */ + async #createTableIfNotExists() { + await this.#connection.schema + .createTable(this.#tableName) + .addColumn('key', 'varchar(255)', (col) => col.primaryKey().notNull()) + .addColumn('owner', 'varchar(255)', (col) => col.notNull()) + .addColumn('expiration', 'bigint') + .ifNotExists() + .execute() + } + + /** + * Compute the expiration date based on the provided TTL + */ + #computeExpiresAt(ttl: number | null) { + if (ttl) return Date.now() + ttl + return null + } + + /** + * Get the current owner of given lock key + */ + async #getCurrentOwner(key: string) { + await this.#initialized + const result = await this.#connection + .selectFrom(this.#tableName) + .where('key', '=', key) + .select('owner') + .executeTakeFirst() + + return result?.owner + } + + /** + * Save the lock in the store if not already locked by another owner + * + * We basically rely on primary key constraint to ensure the lock is + * unique. + * + * If the lock already exists, we check if it's expired. If it is, we + * update it with the new owner and expiration date. + */ + async save(key: string, owner: string, ttl: number | null) { + try { + await this.#initialized + await this.#connection + .insertInto(this.#tableName) + .values({ key, owner, expiration: this.#computeExpiresAt(ttl) }) + .execute() + + return true + } catch (error) { + const updated = await this.#connection + .updateTable(this.#tableName) + .where('key', '=', key) + .where('expiration', '<=', Date.now()) + .set({ owner, expiration: this.#computeExpiresAt(ttl) }) + .executeTakeFirst() + + return updated.numUpdatedRows >= BigInt(1) + } + } + + /** + * Delete the lock from the store if it is owned by the owner + * Otherwise throws a E_LOCK_NOT_OWNED error + */ + async delete(key: string, owner: string): Promise { + const currentOwner = await this.#getCurrentOwner(key) + if (currentOwner !== owner) throw new E_LOCK_NOT_OWNED() + + await this.#connection + .deleteFrom(this.#tableName) + .where('key', '=', key) + .where('owner', '=', owner) + .execute() + } + + /** + * Force delete the lock from the store. No check is made on the owner + */ + async forceDelete(key: string) { + await this.#connection.deleteFrom(this.#tableName).where('key', '=', key).execute() + } + + /** + * Extend the lock expiration. Throws an error if the lock is not owned by the owner + * Duration is in milliseconds + */ + async extend(key: string, owner: string, duration: number) { + const updated = await this.#connection + .updateTable(this.#tableName) + .where('key', '=', key) + .where('owner', '=', owner) + .set({ expiration: Date.now() + duration }) + .executeTakeFirst() + + if (updated.numUpdatedRows === BigInt(0)) throw new E_LOCK_NOT_OWNED() + } + + /** + * Check if the lock exists + */ + async exists(key: string) { + await this.#initialized + const result = await this.#connection + .selectFrom(this.#tableName) + .where('key', '=', key) + .select('expiration') + .executeTakeFirst() + + if (!result) return false + if (result.expiration === null) return true + + return result.expiration > Date.now() + } + + /** + * Disconnect kysely connection + */ + disconnect() { + return this.#connection.destroy() + } +} diff --git a/packages/verrou/src/types/drivers.ts b/packages/verrou/src/types/drivers.ts index e9391e6..2335883 100644 --- a/packages/verrou/src/types/drivers.ts +++ b/packages/verrou/src/types/drivers.ts @@ -1,22 +1,18 @@ -import { type Knex } from 'knex' +import type { Knex } from 'knex' +import type { Kysely } from 'kysely' import type { DynamoDBClientConfig } from '@aws-sdk/client-dynamodb' import type { RedisOptions as IoRedisOptions, Redis as IoRedis } from 'ioredis' export type DialectName = 'pg' | 'mysql2' | 'better-sqlite3' | 'sqlite3' -export type DatabaseStoreOptions = { - /** - * The database dialect - */ - dialect: DialectName - - /** - * The database connection - */ - connection: Knex | Knex.Config['connection'] - +/** + * Common options for database stores + */ +export interface DatabaseOptions { /** * The table name to use ( to store the locks ) + * + * @default 'verrou' */ tableName?: string @@ -28,6 +24,34 @@ export type DatabaseStoreOptions = { autoCreateTable?: boolean } +/** + * Options for the Knex store + */ +export interface KnexStoreOptions extends DatabaseOptions { + /** + * The database dialect + */ + dialect: DialectName + + /** + * The Knex instance + */ + connection: Knex +} + +/** + * Options for the Kysely store + */ +export interface KyselyOptions extends DatabaseOptions { + /** + * The Kysely instance + */ + connection: Kysely +} + +/** + * Options for the Redis store + */ export type RedisStoreOptions = { /** * The Redis connection @@ -35,6 +59,9 @@ export type RedisStoreOptions = { connection: IoRedis | IoRedisOptions } +/** + * Options for the DynamoDB store + */ export type DynamoDbOptions = { /** * DynamoDB table name to use. diff --git a/packages/verrou/test_helpers/index.ts b/packages/verrou/test_helpers/index.ts index 33b337f..ea320c7 100644 --- a/packages/verrou/test_helpers/index.ts +++ b/packages/verrou/test_helpers/index.ts @@ -1,6 +1,3 @@ -import type { Knex } from 'knex' -import type { Group } from '@japa/runner/core' - export const BASE_URL = new URL('./tmp/', import.meta.url) export const REDIS_CREDENTIALS = { @@ -8,18 +5,14 @@ export const REDIS_CREDENTIALS = { port: Number(process.env.REDIS_PORT), } -export function configureDatabaseGroupHooks(db: Knex, group: Group) { - group.each.teardown(async () => { - const exists = await db.schema.hasTable('verrou') - if (!exists) return - - await db.table('verrou').truncate() - }) - - group.teardown(async () => { - const exists = await db.schema.hasTable('verrou') - if (exists) await db.schema.dropTable('verrou') +export const MYSQL_CREDENTIALS = { + user: 'root', + password: 'root', + database: 'mysql', + port: 3306, +} - await db.destroy() - }) +export const POSTGRES_CREDENTIALS = { + user: 'postgres', + password: 'postgres', } diff --git a/packages/verrou/tests/drivers/knex/helpers.ts b/packages/verrou/tests/drivers/knex/helpers.ts new file mode 100644 index 0000000..08e8957 --- /dev/null +++ b/packages/verrou/tests/drivers/knex/helpers.ts @@ -0,0 +1,18 @@ +import type { Knex } from 'knex' +import type { Group } from '@japa/runner/core' + +export function setupTeardownHooks(db: Knex, group: Group) { + group.each.teardown(async () => { + const exists = await db.schema.hasTable('verrou') + if (!exists) return + + await db.table('verrou').truncate() + }) + + group.teardown(async () => { + const exists = await db.schema.hasTable('verrou') + if (exists) await db.schema.dropTable('verrou') + + await db.destroy() + }) +} diff --git a/packages/verrou/tests/drivers/database.spec.ts b/packages/verrou/tests/drivers/knex/knex.spec.ts similarity index 72% rename from packages/verrou/tests/drivers/database.spec.ts rename to packages/verrou/tests/drivers/knex/knex.spec.ts index 6fed0be..933ca6a 100644 --- a/packages/verrou/tests/drivers/database.spec.ts +++ b/packages/verrou/tests/drivers/knex/knex.spec.ts @@ -1,15 +1,15 @@ import knex from 'knex' import { test } from '@japa/runner' -import { DatabaseStore } from '../../src/drivers/database.js' -import { configureDatabaseGroupHooks } from '../../test_helpers/index.js' +import { setupTeardownHooks } from './helpers.js' +import { KnexStore } from '../../../src/drivers/knex.js' const db = knex({ client: 'pg', connection: { user: 'postgres', password: 'postgres' } }) test.group('Database Driver', (group) => { - configureDatabaseGroupHooks(db, group) + setupTeardownHooks(db, group) test('create table with specified tableName', async ({ assert, cleanup }) => { - const store = new DatabaseStore({ + const store = new KnexStore({ connection: db, dialect: 'pg', tableName: 'verrou_my_locks', @@ -26,14 +26,14 @@ test.group('Database Driver', (group) => { }) test('doesnt create table if autoCreateTable is false', async ({ assert }) => { - new DatabaseStore({ connection: db, dialect: 'pg', autoCreateTable: false }) + new KnexStore({ connection: db, dialect: 'pg', autoCreateTable: false }) const hasTable = await db.schema.hasTable('verrou') assert.isFalse(hasTable) }) test('null ttl', async ({ assert }) => { - const store = new DatabaseStore({ connection: db, dialect: 'pg' }) + const store = new KnexStore({ connection: db, dialect: 'pg' }) await store.save('foo', 'bar', null) diff --git a/packages/verrou/tests/drivers/knex/mysql.spec.ts b/packages/verrou/tests/drivers/knex/mysql.spec.ts new file mode 100644 index 0000000..aeeec6f --- /dev/null +++ b/packages/verrou/tests/drivers/knex/mysql.spec.ts @@ -0,0 +1,18 @@ +import knex from 'knex' +import { test } from '@japa/runner' + +import { setupTeardownHooks } from './helpers.js' +import { KnexStore } from '../../../src/drivers/knex.js' +import { MYSQL_CREDENTIALS } from '../../../test_helpers/index.js' +import { registerStoreTestSuite } from '../../../src/test_suite.js' + +const db = knex({ client: 'mysql2', connection: MYSQL_CREDENTIALS }) + +test.group('Mysql driver', (group) => { + setupTeardownHooks(db, group) + registerStoreTestSuite({ + test, + config: { dialect: 'mysql2', connection: db }, + store: KnexStore, + }) +}) diff --git a/packages/verrou/tests/drivers/knex/postgres.spec.ts b/packages/verrou/tests/drivers/knex/postgres.spec.ts new file mode 100644 index 0000000..4fa5b2d --- /dev/null +++ b/packages/verrou/tests/drivers/knex/postgres.spec.ts @@ -0,0 +1,18 @@ +import knex from 'knex' +import { test } from '@japa/runner' + +import { setupTeardownHooks } from './helpers.js' +import { KnexStore } from '../../../src/drivers/knex.js' +import { registerStoreTestSuite } from '../../../src/test_suite.js' +import { POSTGRES_CREDENTIALS } from '../../../test_helpers/index.js' + +const db = knex({ client: 'pg', connection: POSTGRES_CREDENTIALS }) + +test.group('Postgres Driver', (group) => { + setupTeardownHooks(db, group) + registerStoreTestSuite({ + test, + store: KnexStore, + config: { dialect: 'pg', connection: db }, + }) +}) diff --git a/packages/verrou/tests/drivers/sqlite.spec.ts b/packages/verrou/tests/drivers/knex/sqlite.spec.ts similarity index 54% rename from packages/verrou/tests/drivers/sqlite.spec.ts rename to packages/verrou/tests/drivers/knex/sqlite.spec.ts index 76b6163..7896b58 100644 --- a/packages/verrou/tests/drivers/sqlite.spec.ts +++ b/packages/verrou/tests/drivers/knex/sqlite.spec.ts @@ -1,9 +1,9 @@ import knex from 'knex' import { test } from '@japa/runner' -import { DatabaseStore } from '../../src/drivers/database.js' -import { registerStoreTestSuite } from '../../src/test_suite.js' -import { configureDatabaseGroupHooks } from '../../test_helpers/index.js' +import { setupTeardownHooks } from './helpers.js' +import { KnexStore } from '../../../src/drivers/knex.js' +import { registerStoreTestSuite } from '../../../src/test_suite.js' const db = knex({ client: 'sqlite3', @@ -12,10 +12,10 @@ const db = knex({ }) test.group('Sqlite driver', (group) => { - configureDatabaseGroupHooks(db, group) + setupTeardownHooks(db, group) registerStoreTestSuite({ test, config: { dialect: 'sqlite3', connection: db }, - store: DatabaseStore, + store: KnexStore, }) }) diff --git a/packages/verrou/tests/drivers/kysely/helpers.ts b/packages/verrou/tests/drivers/kysely/helpers.ts new file mode 100644 index 0000000..0c7ced0 --- /dev/null +++ b/packages/verrou/tests/drivers/kysely/helpers.ts @@ -0,0 +1,15 @@ +import { sql, type Kysely } from 'kysely' +import type { Group } from '@japa/runner/core' + +export function setupTeardownHooks(group: Group, db: Kysely) { + group.each.teardown(async () => { + sql`DELETE FROM verrou`.execute(db).catch((err) => { + console.error(err) + }) + }) + + group.teardown(async () => { + await db.schema.dropTable('verrou').ifExists().execute() + await db.destroy() + }) +} diff --git a/packages/verrou/tests/drivers/kysely/mysql.spec.ts b/packages/verrou/tests/drivers/kysely/mysql.spec.ts new file mode 100644 index 0000000..965f71d --- /dev/null +++ b/packages/verrou/tests/drivers/kysely/mysql.spec.ts @@ -0,0 +1,21 @@ +import { test } from '@japa/runner' +import { createPool } from 'mysql2' +import { Kysely, MysqlDialect } from 'kysely' + +import { setupTeardownHooks } from './helpers.js' +import { KyselyStore } from '../../../src/drivers/kysely.js' +import { MYSQL_CREDENTIALS } from '../../../test_helpers/index.js' +import { registerStoreTestSuite } from '../../../src/test_suite.js' + +const db = new Kysely({ + dialect: new MysqlDialect({ pool: createPool(MYSQL_CREDENTIALS) }), +}) + +test.group('Kysely | Mysql driver', (group) => { + setupTeardownHooks(group, db) + registerStoreTestSuite({ + test, + config: { connection: db }, + store: KyselyStore, + }) +}) diff --git a/packages/verrou/tests/drivers/kysely/postgres.spec.ts b/packages/verrou/tests/drivers/kysely/postgres.spec.ts new file mode 100644 index 0000000..bc4a4db --- /dev/null +++ b/packages/verrou/tests/drivers/kysely/postgres.spec.ts @@ -0,0 +1,21 @@ +import pg from 'pg' +import { test } from '@japa/runner' +import { Kysely, PostgresDialect } from 'kysely' + +import { setupTeardownHooks } from './helpers.js' +import { KyselyStore } from '../../../src/drivers/kysely.js' +import { registerStoreTestSuite } from '../../../src/test_suite.js' +import { POSTGRES_CREDENTIALS } from '../../../test_helpers/index.js' + +const db = new Kysely({ + dialect: new PostgresDialect({ pool: new pg.Pool(POSTGRES_CREDENTIALS) }), +}) + +test.group('Kysely | Postgres Driver', (group) => { + setupTeardownHooks(group, db) + registerStoreTestSuite({ + test, + store: KyselyStore, + config: { connection: db }, + }) +}) diff --git a/packages/verrou/tests/drivers/kysely/sqlite.spec.ts b/packages/verrou/tests/drivers/kysely/sqlite.spec.ts new file mode 100644 index 0000000..ebbcd84 --- /dev/null +++ b/packages/verrou/tests/drivers/kysely/sqlite.spec.ts @@ -0,0 +1,22 @@ +import { test } from '@japa/runner' +import * as SQLite from 'better-sqlite3' +import { Kysely, SqliteDialect } from 'kysely' + +import { setupTeardownHooks } from './helpers.js' +import { KyselyStore } from '../../../src/drivers/kysely.js' +import { registerStoreTestSuite } from '../../../src/test_suite.js' + +const db = new Kysely({ + dialect: new SqliteDialect({ + database: new SQLite.default('./cache.sqlite3'), + }), +}) + +test.group('Kysely | Sqlite Driver', (group) => { + setupTeardownHooks(group, db) + registerStoreTestSuite({ + test, + store: KyselyStore, + config: { connection: db }, + }) +}) diff --git a/packages/verrou/tests/drivers/mysql.spec.ts b/packages/verrou/tests/drivers/mysql.spec.ts deleted file mode 100644 index fb56a99..0000000 --- a/packages/verrou/tests/drivers/mysql.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import knex from 'knex' -import { test } from '@japa/runner' - -import { DatabaseStore } from '../../src/drivers/database.js' -import { registerStoreTestSuite } from '../../src/test_suite.js' -import { configureDatabaseGroupHooks } from '../../test_helpers/index.js' - -const db = knex({ - client: 'mysql2', - connection: { user: 'root', password: 'root', database: 'mysql', port: 3306 }, -}) - -test.group('Mysql driver', (group) => { - configureDatabaseGroupHooks(db, group) - registerStoreTestSuite({ - test, - config: { dialect: 'mysql2', connection: db }, - store: DatabaseStore, - }) -}) diff --git a/packages/verrou/tests/drivers/postgres.spec.ts b/packages/verrou/tests/drivers/postgres.spec.ts deleted file mode 100644 index 5935af9..0000000 --- a/packages/verrou/tests/drivers/postgres.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import knex from 'knex' -import { test } from '@japa/runner' - -import { DatabaseStore } from '../../src/drivers/database.js' -import { registerStoreTestSuite } from '../../src/test_suite.js' -import { configureDatabaseGroupHooks } from '../../test_helpers/index.js' - -const db = knex({ - client: 'pg', - connection: { user: 'postgres', password: 'postgres' }, -}) - -test.group('Postgres Driver', (group) => { - configureDatabaseGroupHooks(db, group) - registerStoreTestSuite({ - test, - store: DatabaseStore, - config: { dialect: 'pg', connection: db }, - }) -}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b972e2..b54b4a7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -160,15 +160,27 @@ importers: '@aws-sdk/client-dynamodb': specifier: ^3.529.1 version: 3.529.1 + '@types/better-sqlite3': + specifier: ^7.6.9 + version: 7.6.9 + '@types/pg': + specifier: ^8.11.2 + version: 8.11.2 '@types/proper-lockfile': specifier: ^4.1.4 version: 4.1.4 + better-sqlite3: + specifier: ^9.4.3 + version: 9.4.3 ioredis: specifier: ^5.3.2 version: 5.3.2 knex: specifier: ^3.1.0 - version: 3.1.0(mysql2@3.9.2)(pg@8.11.3)(sqlite3@5.1.7) + version: 3.1.0(better-sqlite3@9.4.3)(mysql2@3.9.2)(pg@8.11.3)(sqlite3@5.1.7) + kysely: + specifier: ^0.27.3 + version: 0.27.3 mysql2: specifier: ^3.9.2 version: 3.9.2 @@ -2813,6 +2825,12 @@ packages: resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} dev: true + /@types/better-sqlite3@7.6.9: + resolution: {integrity: sha512-FvktcujPDj9XKMJQWFcl2vVl7OdRIqsSRX9b0acWwTmwLK9CF2eqo/FRcmMLNpugKoX/avA6pb7TorDLmpgTnQ==} + dependencies: + '@types/node': 20.11.25 + dev: true + /@types/bytes@3.1.4: resolution: {integrity: sha512-A0uYgOj3zNc4hNjHc5lYUfJQ/HVyBXiUMKdXd7ysclaE6k9oJdavQzODHuwjpUu2/boCP8afjQYi8z/GtvNCWA==} @@ -2904,6 +2922,14 @@ packages: resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} dev: true + /@types/pg@8.11.2: + resolution: {integrity: sha512-G2Mjygf2jFMU/9hCaTYxJrwdObdcnuQde1gndooZSOHsNSaCehAuwc7EIuSA34Do8Jx2yZ19KtvW8P0j4EuUXw==} + dependencies: + '@types/node': 20.11.25 + pg-protocol: 1.6.0 + pg-types: 4.0.2 + dev: true + /@types/pluralize@0.0.33: resolution: {integrity: sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg==} @@ -3473,6 +3499,14 @@ packages: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} dev: true + /better-sqlite3@9.4.3: + resolution: {integrity: sha512-ud0bTmD9O3uWJGuXDltyj3R47Nz0OHX8iqPOT5PMspGqlu/qQFn+5S2eFBUCrySpavTjFXbi4EgrfVvPAHlImw==} + requiresBuild: true + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.2 + dev: true + /big-integer@1.6.51: resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==} engines: {node: '>=0.6'} @@ -6706,7 +6740,7 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} - /knex@3.1.0(mysql2@3.9.2)(pg@8.11.3)(sqlite3@5.1.7): + /knex@3.1.0(better-sqlite3@9.4.3)(mysql2@3.9.2)(pg@8.11.3)(sqlite3@5.1.7): resolution: {integrity: sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw==} engines: {node: '>=16'} hasBin: true @@ -6734,6 +6768,7 @@ packages: tedious: optional: true dependencies: + better-sqlite3: 9.4.3 colorette: 2.0.19 commander: 10.0.1 debug: 4.3.4 @@ -6755,6 +6790,11 @@ packages: - supports-color dev: true + /kysely@0.27.3: + resolution: {integrity: sha512-lG03Ru+XyOJFsjH3OMY6R/9U38IjDPfnOfDgO3ynhbDr+Dz8fak+X6L62vqu3iybQnj+lG84OttBuU9KY3L9kA==} + engines: {node: '>=14.0.0'} + dev: true + /latest-version@7.0.0: resolution: {integrity: sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==} engines: {node: '>=14.16'} @@ -7903,6 +7943,10 @@ packages: resolution: {integrity: sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==} dev: true + /obuf@1.1.2: + resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + dev: true + /on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} @@ -8285,6 +8329,11 @@ packages: engines: {node: '>=4.0.0'} dev: true + /pg-numeric@1.0.2: + resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} + engines: {node: '>=4'} + dev: true + /pg-pool@3.6.1(pg@8.11.3): resolution: {integrity: sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==} peerDependencies: @@ -8308,6 +8357,19 @@ packages: postgres-interval: 1.2.0 dev: true + /pg-types@4.0.2: + resolution: {integrity: sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==} + engines: {node: '>=10'} + dependencies: + pg-int8: 1.0.1 + pg-numeric: 1.0.2 + postgres-array: 3.0.2 + postgres-bytea: 3.0.0 + postgres-date: 2.1.0 + postgres-interval: 3.0.0 + postgres-range: 1.1.4 + dev: true + /pg@8.11.3: resolution: {integrity: sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==} engines: {node: '>= 8.0.0'} @@ -8454,16 +8516,33 @@ packages: engines: {node: '>=4'} dev: true + /postgres-array@3.0.2: + resolution: {integrity: sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==} + engines: {node: '>=12'} + dev: true + /postgres-bytea@1.0.0: resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} engines: {node: '>=0.10.0'} dev: true + /postgres-bytea@3.0.0: + resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==} + engines: {node: '>= 6'} + dependencies: + obuf: 1.1.2 + dev: true + /postgres-date@1.0.7: resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} engines: {node: '>=0.10.0'} dev: true + /postgres-date@2.1.0: + resolution: {integrity: sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==} + engines: {node: '>=12'} + dev: true + /postgres-interval@1.2.0: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} @@ -8471,6 +8550,15 @@ packages: xtend: 4.0.2 dev: true + /postgres-interval@3.0.0: + resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==} + engines: {node: '>=12'} + dev: true + + /postgres-range@1.1.4: + resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} + dev: true + /preact@10.19.3: resolution: {integrity: sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==} dev: true From 31c5bf77f4c4f3f4223bea25adbdbc3ea115a352 Mon Sep 17 00:00:00 2001 From: Julien Ripouteau Date: Mon, 11 Mar 2024 01:43:46 +0100 Subject: [PATCH 2/9] style: lint --- docs/bin/serve.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/bin/serve.ts b/docs/bin/serve.ts index 27d0bc2..1d6bffc 100644 --- a/docs/bin/serve.ts +++ b/docs/bin/serve.ts @@ -14,7 +14,7 @@ import 'reflect-metadata' import { Ignitor } from '@adonisjs/core' import { readFile } from 'node:fs/promises' import { defineConfig } from '@adonisjs/vite' -import { ApplicationService } from '@adonisjs/core/types' +import type { ApplicationService } from '@adonisjs/core/types' import { defineConfig as defineHttpConfig } from '@adonisjs/core/http' /** @@ -50,7 +50,7 @@ async function defineRoutes(app: ApplicationService) { result[from] = to return result }, - {} as Record + {} as Record, ) router.get('*', async ({ request, response }) => { From d905df01d169c71cb70bec20389d485f1fd2c499 Mon Sep 17 00:00:00 2001 From: Julien Ripouteau Date: Mon, 11 Mar 2024 21:23:06 +0100 Subject: [PATCH 3/9] chore: fix typos --- packages/verrou/src/drivers/kysely.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/verrou/src/drivers/kysely.ts b/packages/verrou/src/drivers/kysely.ts index 81c7d10..662634f 100644 --- a/packages/verrou/src/drivers/kysely.ts +++ b/packages/verrou/src/drivers/kysely.ts @@ -4,15 +4,15 @@ import { E_LOCK_NOT_OWNED } from '../errors.js' import type { KyselyOptions, LockStore } from '../types/main.js' /** - * Create a new knex store + * Create a new Kysely store */ -export function knexStore(config: KyselyOptions) { +export function kyselyStore(config: KyselyOptions) { return { config, factory: () => new KyselyStore(config) } } export class KyselyStore implements LockStore { /** - * Knex connection instance + * Kysely connection instance */ #connection: Kysely @@ -44,7 +44,7 @@ export class KyselyStore implements LockStore { .createTable(this.#tableName) .addColumn('key', 'varchar(255)', (col) => col.primaryKey().notNull()) .addColumn('owner', 'varchar(255)', (col) => col.notNull()) - .addColumn('expiration', 'bigint') + .addColumn('expiration', 'bigint', (col) => col.unsigned()) .ifNotExists() .execute() } From c3a569113d1ab8f77db5e8c049ca5c1c4eac373f Mon Sep 17 00:00:00 2001 From: Julien Ripouteau Date: Tue, 12 Mar 2024 18:29:52 +0100 Subject: [PATCH 4/9] feat: add DatabaseStore + adapter system --- packages/verrou/src/drivers/database.ts | 110 ++++++++++++ packages/verrou/src/drivers/knex.ts | 154 +++++----------- packages/verrou/src/drivers/kysely.ts | 165 ++++++------------ packages/verrou/src/test_suite.ts | 55 +++--- packages/verrou/src/types/drivers.ts | 55 +++++- .../verrou/tests/drivers/dynamodb.spec.ts | 2 +- packages/verrou/tests/drivers/knex/helpers.ts | 9 + .../verrou/tests/drivers/knex/knex.spec.ts | 10 +- .../verrou/tests/drivers/knex/mysql.spec.ts | 6 +- .../tests/drivers/knex/postgres.spec.ts | 6 +- .../verrou/tests/drivers/knex/sqlite.spec.ts | 6 +- .../verrou/tests/drivers/kysely/helpers.ts | 9 + .../verrou/tests/drivers/kysely/mysql.spec.ts | 6 +- .../tests/drivers/kysely/postgres.spec.ts | 6 +- .../tests/drivers/kysely/sqlite.spec.ts | 6 +- packages/verrou/tests/drivers/memory.spec.ts | 3 +- packages/verrou/tests/drivers/redis.spec.ts | 3 +- 17 files changed, 314 insertions(+), 297 deletions(-) create mode 100644 packages/verrou/src/drivers/database.ts diff --git a/packages/verrou/src/drivers/database.ts b/packages/verrou/src/drivers/database.ts new file mode 100644 index 0000000..9e07fc0 --- /dev/null +++ b/packages/verrou/src/drivers/database.ts @@ -0,0 +1,110 @@ +import { E_LOCK_NOT_OWNED } from '../errors.js' +import type { DatabaseAdapter, DatabaseOptions } from '../types/drivers.js' + +/** + * A store that uses a database to store locks + * + * You should provide an adapter that will handle the database interactions + */ +export class DatabaseStore { + #adapter: DatabaseAdapter + #initialized: Promise + + constructor(adapter: DatabaseAdapter, config: DatabaseOptions) { + this.#adapter = adapter + this.#adapter.setTableName(config.tableName || 'verrou') + + if (config.autoCreateTable !== false) { + this.#initialized = this.#adapter.createTableIfNotExists() + } else { + this.#initialized = Promise.resolve() + } + } + + /** + * Compute the expiration date based on the provided TTL + */ + #computeExpiresAt(ttl: number | null) { + if (ttl) return Date.now() + ttl + return null + } + + /** + * Get the current owner of given lock key + */ + async #getCurrentOwner(key: string) { + await this.#initialized + const lock = await this.#adapter.getLock(key) + + return lock?.owner + } + + /** + * Save the lock in the store if not already locked by another owner + * + * We basically rely on primary key constraint to ensure the lock is + * unique. + * + * If the lock already exists, we check if it's expired. If it is, we + * update it with the new owner and expiration date. + */ + async save(key: string, owner: string, ttl: number | null) { + await this.#initialized + try { + await this.#adapter.insertLock({ key, owner, expiration: this.#computeExpiresAt(ttl) }) + return true + } catch (error) { + const updatedRows = await this.#adapter.acquireLock({ + key, + owner, + expiration: this.#computeExpiresAt(ttl), + }) + + return updatedRows > 0 + } + } + + /** + * Delete the lock from the store if it is owned by the owner + * Otherwise throws a E_LOCK_NOT_OWNED error + */ + async delete(key: string, owner: string): Promise { + const currentOwner = await this.#getCurrentOwner(key) + if (currentOwner !== owner) throw new E_LOCK_NOT_OWNED() + + await this.#adapter.deleteLock(key, owner) + } + + /** + * Force delete the lock from the store. No check is made on the owner + */ + async forceDelete(key: string) { + await this.#adapter.deleteLock(key) + } + + /** + * Extend the lock expiration. Throws an error if the lock is not owned by the owner + * Duration is in milliseconds + */ + async extend(key: string, owner: string, duration: number) { + await this.#initialized + const updated = await this.#adapter.extendLock(key, owner, duration) + + if (updated === 0) throw new E_LOCK_NOT_OWNED() + } + + /** + * Check if the lock exists + */ + async exists(key: string) { + await this.#initialized + const lock = await this.#adapter.getLock(key) + + if (!lock) return false + if (lock.expiration === null) return true + + return lock.expiration > Date.now() + } + + async disconnect() {} +} diff --git a/packages/verrou/src/drivers/knex.ts b/packages/verrou/src/drivers/knex.ts index 4f7f176..a5a7840 100644 --- a/packages/verrou/src/drivers/knex.ts +++ b/packages/verrou/src/drivers/knex.ts @@ -1,45 +1,37 @@ import type { Knex } from 'knex' -import { E_LOCK_NOT_OWNED } from '../errors.js' -import type { KnexStoreOptions, LockStore } from '../types/main.js' +import { DatabaseStore } from './database.js' +import type { DatabaseAdapter, KnexStoreOptions } from '../types/main.js' /** - * Create a new database store + * Create a new knex store */ export function knexStore(config: KnexStoreOptions) { - return { config, factory: () => new KnexStore(config) } + return { + config, + factory: () => { + const adapter = new KnexAdapter(config.connection) + return new DatabaseStore(adapter, config) + }, + } } -export class KnexStore implements LockStore { - /** - * Knex connection instance - */ +/** + * Knex adapter for the DatabaseStore + */ +export class KnexAdapter implements DatabaseAdapter { #connection: Knex + #tableName!: string - /** - * The name of the table used to store locks - */ - #tableName = 'verrou' - - /** - * A promise that resolves when the table is created - */ - #initialized: Promise + constructor(connection: Knex) { + this.#connection = connection + } - constructor(config: KnexStoreOptions) { - this.#connection = config.connection - this.#tableName = config.tableName || this.#tableName - if (config.autoCreateTable !== false) { - this.#initialized = this.#createTableIfNotExists() - } else { - this.#initialized = Promise.resolve() - } + setTableName(tableName: string) { + this.#tableName = tableName } - /** - * Create the locks table if it doesn't exist - */ - async #createTableIfNotExists() { + async createTableIfNotExists() { const hasTable = await this.#connection.schema.hasTable(this.#tableName) if (hasTable) return @@ -50,109 +42,47 @@ export class KnexStore implements LockStore { }) } - /** - * Compute the expiration date based on the provided TTL - */ - #computeExpiresAt(ttl: number | null) { - if (ttl) return Date.now() + ttl - return null + async insertLock(lock: { key: string; owner: string; expiration: number | null }) { + await this.#connection.table(this.#tableName).insert(lock) } - /** - * Get the current owner of given lock key - */ - async #getCurrentOwner(key: string) { - await this.#initialized - const result = await this.#connection + async acquireLock(lock: { key: string; owner: string; expiration: number | null }) { + const updated = await this.#connection .table(this.#tableName) - .where('key', key) - .select('owner') - .first() + .where('key', lock.key) + .where('expiration', '<=', Date.now()) + .update({ owner: lock.owner, expiration: lock.expiration }) - return result?.owner + return updated } - /** - * Save the lock in the store if not already locked by another owner - * - * We basically rely on primary key constraint to ensure the lock is - * unique. - * - * If the lock already exists, we check if it's expired. If it is, we - * update it with the new owner and expiration date. - */ - async save(key: string, owner: string, ttl: number | null) { - try { - await this.#initialized - await this.#connection - .table(this.#tableName) - .insert({ key, owner, expiration: this.#computeExpiresAt(ttl) }) - - return true - } catch (error) { - const updated = await this.#connection - .table(this.#tableName) - .where('key', key) - .where('expiration', '<=', Date.now()) - .update({ owner, expiration: this.#computeExpiresAt(ttl) }) - - return updated >= 1 - } - } - - /** - * Delete the lock from the store if it is owned by the owner - * Otherwise throws a E_LOCK_NOT_OWNED error - */ - async delete(key: string, owner: string): Promise { - const currentOwner = await this.#getCurrentOwner(key) - if (currentOwner !== owner) throw new E_LOCK_NOT_OWNED() - - await this.#connection.table(this.#tableName).where('key', key).where('owner', owner).delete() - } - - /** - * Force delete the lock from the store. No check is made on the owner - */ - async forceDelete(key: string) { - await this.#connection.table(this.#tableName).where('key', key).delete() + async deleteLock(key: string, owner?: string | undefined) { + await this.#connection + .table(this.#tableName) + .where('key', key) + .modify((query) => { + if (owner) query.where('owner', owner) + }) + .delete() } - /** - * Extend the lock expiration. Throws an error if the lock is not owned by the owner - * Duration is in milliseconds - */ - async extend(key: string, owner: string, duration: number) { + async extendLock(key: string, owner: string, duration: number) { const updated = await this.#connection .table(this.#tableName) .where('key', key) .where('owner', owner) .update({ expiration: Date.now() + duration }) - if (updated === 0) throw new E_LOCK_NOT_OWNED() + return updated } - /** - * Check if the lock exists - */ - async exists(key: string) { - await this.#initialized + async getLock(key: string) { const result = await this.#connection .table(this.#tableName) .where('key', key) - .select('expiration') + .select(['owner', 'expiration']) .first() - if (!result) return false - if (result.expiration === null) return true - - return result.expiration > Date.now() - } - - /** - * Disconnect knex connection - */ - disconnect() { - return this.#connection.destroy() + return result } } diff --git a/packages/verrou/src/drivers/kysely.ts b/packages/verrou/src/drivers/kysely.ts index 662634f..0cb6930 100644 --- a/packages/verrou/src/drivers/kysely.ts +++ b/packages/verrou/src/drivers/kysely.ts @@ -1,133 +1,82 @@ -import type { Kysely } from 'kysely' +import { PostgresAdapter, type Kysely } from 'kysely' -import { E_LOCK_NOT_OWNED } from '../errors.js' -import type { KyselyOptions, LockStore } from '../types/main.js' +import { DatabaseStore } from './database.js' +import type { DatabaseAdapter, KyselyOptions } from '../types/main.js' /** * Create a new Kysely store */ export function kyselyStore(config: KyselyOptions) { - return { config, factory: () => new KyselyStore(config) } + return { + config, + factory: () => { + const adapter = new KyselyAdapter(config.connection) + return new DatabaseStore(adapter, config) + }, + } } -export class KyselyStore implements LockStore { - /** - * Kysely connection instance - */ +/** + * Kysely adapter for the DatabaseStore + */ +export class KyselyAdapter implements DatabaseAdapter { #connection: Kysely + #tableName!: string - /** - * The name of the table used to store locks - */ - #tableName = 'verrou' - - /** - * A promise that resolves when the table is created - */ - #initialized: Promise + constructor(connection: Kysely) { + this.#connection = connection + } - constructor(config: KyselyOptions) { - this.#connection = config.connection - this.#tableName = config.tableName || this.#tableName - if (config.autoCreateTable !== false) { - this.#initialized = this.#createTableIfNotExists() - } else { - this.#initialized = Promise.resolve() - } + setTableName(tableName: string) { + this.#tableName = tableName } - /** - * Create the locks table if it doesn't exist - */ - async #createTableIfNotExists() { + async createTableIfNotExists() { + const isPg = this.#connection.getExecutor().adapter instanceof PostgresAdapter + await this.#connection.schema .createTable(this.#tableName) .addColumn('key', 'varchar(255)', (col) => col.primaryKey().notNull()) .addColumn('owner', 'varchar(255)', (col) => col.notNull()) - .addColumn('expiration', 'bigint', (col) => col.unsigned()) + .addColumn('expiration', 'bigint', (col) => { + if (!isPg) col.unsigned() + return col + }) .ifNotExists() .execute() } - /** - * Compute the expiration date based on the provided TTL - */ - #computeExpiresAt(ttl: number | null) { - if (ttl) return Date.now() + ttl - return null + async insertLock(lock: { key: string; owner: string; expiration: number | null }) { + await this.#connection + .insertInto(this.#tableName) + .values({ + key: lock.key, + owner: lock.owner, + expiration: lock.expiration, + }) + .execute() } - /** - * Get the current owner of given lock key - */ - async #getCurrentOwner(key: string) { - await this.#initialized - const result = await this.#connection - .selectFrom(this.#tableName) - .where('key', '=', key) - .select('owner') + async acquireLock(lock: { key: string; owner: string; expiration: number | null }) { + const updated = await this.#connection + .updateTable(this.#tableName) + .where('key', '=', lock.key) + .where('expiration', '<=', Date.now()) + .set({ owner: lock.owner, expiration: lock.expiration }) .executeTakeFirst() - return result?.owner - } - - /** - * Save the lock in the store if not already locked by another owner - * - * We basically rely on primary key constraint to ensure the lock is - * unique. - * - * If the lock already exists, we check if it's expired. If it is, we - * update it with the new owner and expiration date. - */ - async save(key: string, owner: string, ttl: number | null) { - try { - await this.#initialized - await this.#connection - .insertInto(this.#tableName) - .values({ key, owner, expiration: this.#computeExpiresAt(ttl) }) - .execute() - - return true - } catch (error) { - const updated = await this.#connection - .updateTable(this.#tableName) - .where('key', '=', key) - .where('expiration', '<=', Date.now()) - .set({ owner, expiration: this.#computeExpiresAt(ttl) }) - .executeTakeFirst() - - return updated.numUpdatedRows >= BigInt(1) - } + return Number(updated.numUpdatedRows) } - /** - * Delete the lock from the store if it is owned by the owner - * Otherwise throws a E_LOCK_NOT_OWNED error - */ - async delete(key: string, owner: string): Promise { - const currentOwner = await this.#getCurrentOwner(key) - if (currentOwner !== owner) throw new E_LOCK_NOT_OWNED() - + async deleteLock(key: string, owner?: string | undefined) { await this.#connection .deleteFrom(this.#tableName) .where('key', '=', key) - .where('owner', '=', owner) + .$if(owner !== undefined, (query) => query.where('owner', '=', owner)) .execute() } - /** - * Force delete the lock from the store. No check is made on the owner - */ - async forceDelete(key: string) { - await this.#connection.deleteFrom(this.#tableName).where('key', '=', key).execute() - } - - /** - * Extend the lock expiration. Throws an error if the lock is not owned by the owner - * Duration is in milliseconds - */ - async extend(key: string, owner: string, duration: number) { + async extendLock(key: string, owner: string, duration: number) { const updated = await this.#connection .updateTable(this.#tableName) .where('key', '=', key) @@ -135,30 +84,16 @@ export class KyselyStore implements LockStore { .set({ expiration: Date.now() + duration }) .executeTakeFirst() - if (updated.numUpdatedRows === BigInt(0)) throw new E_LOCK_NOT_OWNED() + return Number(updated.numUpdatedRows) } - /** - * Check if the lock exists - */ - async exists(key: string) { - await this.#initialized + async getLock(key: string) { const result = await this.#connection .selectFrom(this.#tableName) .where('key', '=', key) - .select('expiration') + .select(['owner', 'expiration']) .executeTakeFirst() - if (!result) return false - if (result.expiration === null) return true - - return result.expiration > Date.now() - } - - /** - * Disconnect kysely connection - */ - disconnect() { - return this.#connection.destroy() + return result } } diff --git a/packages/verrou/src/test_suite.ts b/packages/verrou/src/test_suite.ts index 6f8979e..6e0c0c6 100644 --- a/packages/verrou/src/test_suite.ts +++ b/packages/verrou/src/test_suite.ts @@ -8,16 +8,15 @@ import { LockFactory } from './lock_factory.js' import type { LockStore } from './types/main.js' import { E_LOCK_NOT_OWNED, E_LOCK_TIMEOUT } from '../index.js' -export function registerStoreTestSuite(options: { +export function registerStoreTestSuite(options: { test: typeof JapaTest - store: T - config: ConstructorParameters[0] + createStore: () => LockStore configureGroup?: (group: Group) => any }) { - const { test, store, config } = options + const { test } = options test('acquiring lock is exclusive', async ({ assert }) => { - const provider = new LockFactory(new store(config)) + const provider = new LockFactory(options.createStore()) const lock = provider.createLock('foo') await lock.acquire() @@ -36,14 +35,14 @@ export function registerStoreTestSuite { - const provider = new LockFactory(new store(config)) + const provider = new LockFactory(options.createStore()) const lock = provider.createLock('foo') assert.isFalse(await lock.isLocked()) }) test('acquiring lock makes it locked', async ({ assert }) => { - const provider = new LockFactory(new store(config)) + const provider = new LockFactory(options.createStore()) const lock = provider.createLock('foo') await lock.acquire() @@ -52,7 +51,7 @@ export function registerStoreTestSuite { - const provider = new LockFactory(new store(config)) + const provider = new LockFactory(options.createStore()) const lock1 = provider.createLock('foo1') const lock2 = provider.createLock('foo1') @@ -64,7 +63,7 @@ export function registerStoreTestSuite { - const provider = new LockFactory(new store(config)) + const provider = new LockFactory(options.createStore()) const lock = provider.createLock('foo') const lock2 = provider.createLock('foo') @@ -75,7 +74,7 @@ export function registerStoreTestSuite { - const provider = new LockFactory(new store(config), { + const provider = new LockFactory(options.createStore(), { retry: { timeout: 500 }, }) const lock = provider.createLock('foo') @@ -87,7 +86,7 @@ export function registerStoreTestSuite { - const provider = new LockFactory(new store(config)) + const provider = new LockFactory(options.createStore()) const lock = provider.createLock('foo') const result = await lock.run(async () => 'hello world') @@ -96,7 +95,7 @@ export function registerStoreTestSuite { - const provider = new LockFactory(new store(config)) + const provider = new LockFactory(options.createStore()) const lock = provider.createLock('foo') const result = await lock.run(async () => Promise.resolve('hello world')) @@ -105,7 +104,7 @@ export function registerStoreTestSuite { - const provider = new LockFactory(new store(config)) + const provider = new LockFactory(options.createStore()) const lock = provider.createLock('foo') await assert.rejects( @@ -118,7 +117,7 @@ export function registerStoreTestSuite { - const provider = new LockFactory(new store(config)) + const provider = new LockFactory(options.createStore()) const lock = provider.createLock('foo') await assert.rejects(async () => { @@ -129,7 +128,7 @@ export function registerStoreTestSuite { - const provider = new LockFactory(new store(config)) + const provider = new LockFactory(options.createStore()) const lock = provider.createLock('foo') let flag = false @@ -150,7 +149,7 @@ export function registerStoreTestSuite { - const provider = new LockFactory(new store(config)) + const provider = new LockFactory(options.createStore()) const lock = provider.createLock('foo') let flag = false @@ -167,7 +166,7 @@ export function registerStoreTestSuite { let v = 0 - const provider = new LockFactory(new store(config)) + const provider = new LockFactory(options.createStore()) const lock = provider.createLock('foo') const run = async () => { @@ -181,14 +180,14 @@ export function registerStoreTestSuite { - const provider = new LockFactory(new store(config)) + const provider = new LockFactory(options.createStore()) const lock = provider.createLock('foo') assert.isString(lock.getOwner()) }) test('restore lock from another instance', async ({ assert }) => { - const storeInstance = new store(config) + const storeInstance = options.createStore() const provider = new LockFactory(storeInstance) const lock = provider.createLock('foo') @@ -204,7 +203,7 @@ export function registerStoreTestSuite { - const provider = new LockFactory(new store(config)) + const provider = new LockFactory(options.createStore()) const lock = provider.createLock('foo', 1000) await lock.acquire() @@ -217,7 +216,7 @@ export function registerStoreTestSuite { - const storeInstance = new store(config) + const storeInstance = options.createStore() const provider = new LockFactory(storeInstance, { retry: { delay: 25 } }) const lock1 = provider.createLock('foo', 1000) const lock2 = provider.createLock('foo', 1000) @@ -234,7 +233,7 @@ export function registerStoreTestSuite { - const storeInstance = new store(config) + const storeInstance = options.createStore() const provider = new LockFactory(storeInstance) const lock1 = provider.createLock('foo', 1000) const lock2 = provider.createLock('foo', 1000) @@ -247,7 +246,7 @@ export function registerStoreTestSuite { - const provider = new LockFactory(new store(config)) + const provider = new LockFactory(options.createStore()) const lock = provider.createLock('foo', 400) await lock.acquire() @@ -260,7 +259,7 @@ export function registerStoreTestSuite { - const provider = new LockFactory(new store(config), { + const provider = new LockFactory(options.createStore(), { retry: { attempts: 1, }, @@ -283,7 +282,7 @@ export function registerStoreTestSuite { - const provider = new LockFactory(new store(config)) + const provider = new LockFactory(options.createStore()) const lock = provider.createLock('foo', null) await lock.acquire() @@ -296,7 +295,7 @@ export function registerStoreTestSuite { - const provider = new LockFactory(new store(config)) + const provider = new LockFactory(options.createStore()) const lock = provider.createLock('foo') const lock2 = provider.createLock('foo') @@ -308,7 +307,7 @@ export function registerStoreTestSuite { - const provider = new LockFactory(new store(config)) + const provider = new LockFactory(options.createStore()) const lock = provider.createLock('foo', 1000) await lock.acquire() @@ -330,7 +329,7 @@ export function registerStoreTestSuite { - const provider = new LockFactory(new store(config)) + const provider = new LockFactory(options.createStore()) const lock = provider.createLock('foo', 1000) await lock.extend() diff --git a/packages/verrou/src/types/drivers.ts b/packages/verrou/src/types/drivers.ts index 2335883..2c4e938 100644 --- a/packages/verrou/src/types/drivers.ts +++ b/packages/verrou/src/types/drivers.ts @@ -3,8 +3,6 @@ import type { Kysely } from 'kysely' import type { DynamoDBClientConfig } from '@aws-sdk/client-dynamodb' import type { RedisOptions as IoRedisOptions, Redis as IoRedis } from 'ioredis' -export type DialectName = 'pg' | 'mysql2' | 'better-sqlite3' | 'sqlite3' - /** * Common options for database stores */ @@ -28,11 +26,6 @@ export interface DatabaseOptions { * Options for the Knex store */ export interface KnexStoreOptions extends DatabaseOptions { - /** - * The database dialect - */ - dialect: DialectName - /** * The Knex instance */ @@ -85,3 +78,51 @@ export type DynamoDbOptions = { */ endpoint: DynamoDBClientConfig['endpoint'] } + +/** + * An adapter for the DatabaseStore + */ +export interface DatabaseAdapter { + /** + * Set the table name to store the locks + */ + setTableName(tableName: string): void + + /** + * Create the table to store the locks if it doesn't exist + */ + createTableIfNotExists(): Promise + + /** + * Insert the given lock in the store + */ + insertLock(lock: { key: string; owner: string; expiration: number | null }): Promise + + /** + * Acquire the lock by updating the owner and expiration date. + * + * The adapter should check if expiration date is in the past + * and return the number of updated rows. + */ + acquireLock(lock: { key: string; owner: string; expiration: number | null }): Promise + + /** + * Delete a lock from the store. + * + * If owner is provided, the lock should only be deleted if the owner matches. + */ + deleteLock(key: string, owner?: string): Promise + + /** + * Extend the expiration date of the lock by the given + * duration ( Date.now() + duration ). + * + * The owner must match. + */ + extendLock(key: string, owner: string, duration: number): Promise + + /** + * Returns the current owner and expiration date of the lock + */ + getLock(key: string): Promise<{ owner: string; expiration: number | null } | undefined> +} diff --git a/packages/verrou/tests/drivers/dynamodb.spec.ts b/packages/verrou/tests/drivers/dynamodb.spec.ts index 5611715..840e036 100644 --- a/packages/verrou/tests/drivers/dynamodb.spec.ts +++ b/packages/verrou/tests/drivers/dynamodb.spec.ts @@ -22,7 +22,7 @@ function deleteTableTeardown(tableName: string) { test.group('DynamoDB Store', (group) => { group.each.teardown(deleteTableTeardown('verrou')) - registerStoreTestSuite({ test, config, store: DynamoDBStore }) + registerStoreTestSuite({ test, createStore: () => new DynamoDBStore(config) }) test('should automatically create table', async ({ assert }) => { const store = new DynamoDBStore(config) diff --git a/packages/verrou/tests/drivers/knex/helpers.ts b/packages/verrou/tests/drivers/knex/helpers.ts index 08e8957..127e3f1 100644 --- a/packages/verrou/tests/drivers/knex/helpers.ts +++ b/packages/verrou/tests/drivers/knex/helpers.ts @@ -1,6 +1,10 @@ import type { Knex } from 'knex' import type { Group } from '@japa/runner/core' +import { KnexAdapter } from '../../../src/drivers/knex.js' +import type { KnexStoreOptions } from '../../../src/types/drivers.js' +import { DatabaseStore } from '../../../src/drivers/database.js' + export function setupTeardownHooks(db: Knex, group: Group) { group.each.teardown(async () => { const exists = await db.schema.hasTable('verrou') @@ -16,3 +20,8 @@ export function setupTeardownHooks(db: Knex, group: Group) { await db.destroy() }) } + +export function createKnexStore(options: KnexStoreOptions) { + const adapter = new KnexAdapter(options.connection) + return new DatabaseStore(adapter, options) +} diff --git a/packages/verrou/tests/drivers/knex/knex.spec.ts b/packages/verrou/tests/drivers/knex/knex.spec.ts index 933ca6a..bd9c937 100644 --- a/packages/verrou/tests/drivers/knex/knex.spec.ts +++ b/packages/verrou/tests/drivers/knex/knex.spec.ts @@ -1,17 +1,15 @@ import knex from 'knex' import { test } from '@japa/runner' -import { setupTeardownHooks } from './helpers.js' -import { KnexStore } from '../../../src/drivers/knex.js' +import { createKnexStore, setupTeardownHooks } from './helpers.js' const db = knex({ client: 'pg', connection: { user: 'postgres', password: 'postgres' } }) test.group('Database Driver', (group) => { setupTeardownHooks(db, group) test('create table with specified tableName', async ({ assert, cleanup }) => { - const store = new KnexStore({ + const store = createKnexStore({ connection: db, - dialect: 'pg', tableName: 'verrou_my_locks', }) @@ -26,14 +24,14 @@ test.group('Database Driver', (group) => { }) test('doesnt create table if autoCreateTable is false', async ({ assert }) => { - new KnexStore({ connection: db, dialect: 'pg', autoCreateTable: false }) + createKnexStore({ connection: db, autoCreateTable: false }) const hasTable = await db.schema.hasTable('verrou') assert.isFalse(hasTable) }) test('null ttl', async ({ assert }) => { - const store = new KnexStore({ connection: db, dialect: 'pg' }) + const store = createKnexStore({ connection: db }) await store.save('foo', 'bar', null) diff --git a/packages/verrou/tests/drivers/knex/mysql.spec.ts b/packages/verrou/tests/drivers/knex/mysql.spec.ts index aeeec6f..20fb194 100644 --- a/packages/verrou/tests/drivers/knex/mysql.spec.ts +++ b/packages/verrou/tests/drivers/knex/mysql.spec.ts @@ -1,8 +1,7 @@ import knex from 'knex' import { test } from '@japa/runner' -import { setupTeardownHooks } from './helpers.js' -import { KnexStore } from '../../../src/drivers/knex.js' +import { createKnexStore, setupTeardownHooks } from './helpers.js' import { MYSQL_CREDENTIALS } from '../../../test_helpers/index.js' import { registerStoreTestSuite } from '../../../src/test_suite.js' @@ -12,7 +11,6 @@ test.group('Mysql driver', (group) => { setupTeardownHooks(db, group) registerStoreTestSuite({ test, - config: { dialect: 'mysql2', connection: db }, - store: KnexStore, + createStore: () => createKnexStore({ connection: db }), }) }) diff --git a/packages/verrou/tests/drivers/knex/postgres.spec.ts b/packages/verrou/tests/drivers/knex/postgres.spec.ts index 4fa5b2d..80d745c 100644 --- a/packages/verrou/tests/drivers/knex/postgres.spec.ts +++ b/packages/verrou/tests/drivers/knex/postgres.spec.ts @@ -1,8 +1,7 @@ import knex from 'knex' import { test } from '@japa/runner' -import { setupTeardownHooks } from './helpers.js' -import { KnexStore } from '../../../src/drivers/knex.js' +import { createKnexStore, setupTeardownHooks } from './helpers.js' import { registerStoreTestSuite } from '../../../src/test_suite.js' import { POSTGRES_CREDENTIALS } from '../../../test_helpers/index.js' @@ -12,7 +11,6 @@ test.group('Postgres Driver', (group) => { setupTeardownHooks(db, group) registerStoreTestSuite({ test, - store: KnexStore, - config: { dialect: 'pg', connection: db }, + createStore: () => createKnexStore({ connection: db }), }) }) diff --git a/packages/verrou/tests/drivers/knex/sqlite.spec.ts b/packages/verrou/tests/drivers/knex/sqlite.spec.ts index 7896b58..2fdbc60 100644 --- a/packages/verrou/tests/drivers/knex/sqlite.spec.ts +++ b/packages/verrou/tests/drivers/knex/sqlite.spec.ts @@ -1,8 +1,7 @@ import knex from 'knex' import { test } from '@japa/runner' -import { setupTeardownHooks } from './helpers.js' -import { KnexStore } from '../../../src/drivers/knex.js' +import { createKnexStore, setupTeardownHooks } from './helpers.js' import { registerStoreTestSuite } from '../../../src/test_suite.js' const db = knex({ @@ -15,7 +14,6 @@ test.group('Sqlite driver', (group) => { setupTeardownHooks(db, group) registerStoreTestSuite({ test, - config: { dialect: 'sqlite3', connection: db }, - store: KnexStore, + createStore: () => createKnexStore({ connection: db, dialect: 'sqlite3' }), }) }) diff --git a/packages/verrou/tests/drivers/kysely/helpers.ts b/packages/verrou/tests/drivers/kysely/helpers.ts index 0c7ced0..d30e056 100644 --- a/packages/verrou/tests/drivers/kysely/helpers.ts +++ b/packages/verrou/tests/drivers/kysely/helpers.ts @@ -1,6 +1,10 @@ import { sql, type Kysely } from 'kysely' import type { Group } from '@japa/runner/core' +import type { KyselyOptions } from '../../../src/types/drivers.js' +import { KyselyAdapter } from '../../../src/drivers/kysely.js' +import { DatabaseStore } from '../../../src/drivers/database.js' + export function setupTeardownHooks(group: Group, db: Kysely) { group.each.teardown(async () => { sql`DELETE FROM verrou`.execute(db).catch((err) => { @@ -13,3 +17,8 @@ export function setupTeardownHooks(group: Group, db: Kysely) { await db.destroy() }) } + +export function createKyselyStore(options: KyselyOptions) { + const adapter = new KyselyAdapter(options.connection) + return new DatabaseStore(adapter, options) +} diff --git a/packages/verrou/tests/drivers/kysely/mysql.spec.ts b/packages/verrou/tests/drivers/kysely/mysql.spec.ts index 965f71d..b0ab3f7 100644 --- a/packages/verrou/tests/drivers/kysely/mysql.spec.ts +++ b/packages/verrou/tests/drivers/kysely/mysql.spec.ts @@ -2,10 +2,9 @@ import { test } from '@japa/runner' import { createPool } from 'mysql2' import { Kysely, MysqlDialect } from 'kysely' -import { setupTeardownHooks } from './helpers.js' -import { KyselyStore } from '../../../src/drivers/kysely.js' import { MYSQL_CREDENTIALS } from '../../../test_helpers/index.js' import { registerStoreTestSuite } from '../../../src/test_suite.js' +import { createKyselyStore, setupTeardownHooks } from './helpers.js' const db = new Kysely({ dialect: new MysqlDialect({ pool: createPool(MYSQL_CREDENTIALS) }), @@ -15,7 +14,6 @@ test.group('Kysely | Mysql driver', (group) => { setupTeardownHooks(group, db) registerStoreTestSuite({ test, - config: { connection: db }, - store: KyselyStore, + createStore: () => createKyselyStore({ connection: db }), }) }) diff --git a/packages/verrou/tests/drivers/kysely/postgres.spec.ts b/packages/verrou/tests/drivers/kysely/postgres.spec.ts index bc4a4db..3388064 100644 --- a/packages/verrou/tests/drivers/kysely/postgres.spec.ts +++ b/packages/verrou/tests/drivers/kysely/postgres.spec.ts @@ -2,9 +2,8 @@ import pg from 'pg' import { test } from '@japa/runner' import { Kysely, PostgresDialect } from 'kysely' -import { setupTeardownHooks } from './helpers.js' -import { KyselyStore } from '../../../src/drivers/kysely.js' import { registerStoreTestSuite } from '../../../src/test_suite.js' +import { createKyselyStore, setupTeardownHooks } from './helpers.js' import { POSTGRES_CREDENTIALS } from '../../../test_helpers/index.js' const db = new Kysely({ @@ -15,7 +14,6 @@ test.group('Kysely | Postgres Driver', (group) => { setupTeardownHooks(group, db) registerStoreTestSuite({ test, - store: KyselyStore, - config: { connection: db }, + createStore: () => createKyselyStore({ connection: db }), }) }) diff --git a/packages/verrou/tests/drivers/kysely/sqlite.spec.ts b/packages/verrou/tests/drivers/kysely/sqlite.spec.ts index ebbcd84..1f4aa07 100644 --- a/packages/verrou/tests/drivers/kysely/sqlite.spec.ts +++ b/packages/verrou/tests/drivers/kysely/sqlite.spec.ts @@ -2,9 +2,8 @@ import { test } from '@japa/runner' import * as SQLite from 'better-sqlite3' import { Kysely, SqliteDialect } from 'kysely' -import { setupTeardownHooks } from './helpers.js' -import { KyselyStore } from '../../../src/drivers/kysely.js' import { registerStoreTestSuite } from '../../../src/test_suite.js' +import { createKyselyStore, setupTeardownHooks } from './helpers.js' const db = new Kysely({ dialect: new SqliteDialect({ @@ -16,7 +15,6 @@ test.group('Kysely | Sqlite Driver', (group) => { setupTeardownHooks(group, db) registerStoreTestSuite({ test, - store: KyselyStore, - config: { connection: db }, + createStore: () => createKyselyStore({ connection: db }), }) }) diff --git a/packages/verrou/tests/drivers/memory.spec.ts b/packages/verrou/tests/drivers/memory.spec.ts index a48ea51..f2f92fc 100644 --- a/packages/verrou/tests/drivers/memory.spec.ts +++ b/packages/verrou/tests/drivers/memory.spec.ts @@ -6,7 +6,6 @@ import { registerStoreTestSuite } from '../../src/test_suite.js' test.group('Memory Store', () => { registerStoreTestSuite({ test, - config: undefined, - store: MemoryStore, + createStore: () => new MemoryStore(), }) }) diff --git a/packages/verrou/tests/drivers/redis.spec.ts b/packages/verrou/tests/drivers/redis.spec.ts index 221c938..081faf0 100644 --- a/packages/verrou/tests/drivers/redis.spec.ts +++ b/packages/verrou/tests/drivers/redis.spec.ts @@ -18,8 +18,7 @@ test.group('Redis Driver', (group) => { registerStoreTestSuite({ test, - config: { connection: ioredis }, - store: RedisStore, + createStore: () => new RedisStore({ connection: ioredis }), }) test('null ttl', async ({ assert }) => { From 6e6ff056726d230cdab8c06c078e7251e8871bf0 Mon Sep 17 00:00:00 2001 From: Julien Ripouteau Date: Tue, 12 Mar 2024 18:30:24 +0100 Subject: [PATCH 5/9] style: lint files --- packages/verrou/tests/drivers/knex/helpers.ts | 2 +- packages/verrou/tests/drivers/knex/sqlite.spec.ts | 2 +- packages/verrou/tests/drivers/kysely/helpers.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/verrou/tests/drivers/knex/helpers.ts b/packages/verrou/tests/drivers/knex/helpers.ts index 127e3f1..a3f80dd 100644 --- a/packages/verrou/tests/drivers/knex/helpers.ts +++ b/packages/verrou/tests/drivers/knex/helpers.ts @@ -2,8 +2,8 @@ import type { Knex } from 'knex' import type { Group } from '@japa/runner/core' import { KnexAdapter } from '../../../src/drivers/knex.js' -import type { KnexStoreOptions } from '../../../src/types/drivers.js' import { DatabaseStore } from '../../../src/drivers/database.js' +import type { KnexStoreOptions } from '../../../src/types/drivers.js' export function setupTeardownHooks(db: Knex, group: Group) { group.each.teardown(async () => { diff --git a/packages/verrou/tests/drivers/knex/sqlite.spec.ts b/packages/verrou/tests/drivers/knex/sqlite.spec.ts index 2fdbc60..cb080cb 100644 --- a/packages/verrou/tests/drivers/knex/sqlite.spec.ts +++ b/packages/verrou/tests/drivers/knex/sqlite.spec.ts @@ -14,6 +14,6 @@ test.group('Sqlite driver', (group) => { setupTeardownHooks(db, group) registerStoreTestSuite({ test, - createStore: () => createKnexStore({ connection: db, dialect: 'sqlite3' }), + createStore: () => createKnexStore({ connection: db }), }) }) diff --git a/packages/verrou/tests/drivers/kysely/helpers.ts b/packages/verrou/tests/drivers/kysely/helpers.ts index d30e056..4603e8b 100644 --- a/packages/verrou/tests/drivers/kysely/helpers.ts +++ b/packages/verrou/tests/drivers/kysely/helpers.ts @@ -1,9 +1,9 @@ import { sql, type Kysely } from 'kysely' import type { Group } from '@japa/runner/core' -import type { KyselyOptions } from '../../../src/types/drivers.js' import { KyselyAdapter } from '../../../src/drivers/kysely.js' import { DatabaseStore } from '../../../src/drivers/database.js' +import type { KyselyOptions } from '../../../src/types/drivers.js' export function setupTeardownHooks(group: Group, db: Kysely) { group.each.teardown(async () => { From c96d91e12702182476630f20d72cab75648cb9c3 Mon Sep 17 00:00:00 2001 From: Julien Ripouteau Date: Tue, 12 Mar 2024 20:09:59 +0100 Subject: [PATCH 6/9] refactor: remove all disconnect features --- docs/content/docs/api.md | 16 --------------- docs/content/docs/extend/custom_lock_store.md | 1 - packages/verrou/src/drivers/database.ts | 2 -- packages/verrou/src/drivers/dynamodb.ts | 7 ------- packages/verrou/src/drivers/memory.ts | 4 ---- packages/verrou/src/drivers/redis.ts | 7 ------- packages/verrou/src/lock_factory.ts | 7 ------- packages/verrou/src/types/main.ts | 5 ----- packages/verrou/src/verrou.ts | 20 ------------------- packages/verrou/test_helpers/null_store.ts | 4 ---- playground/src/index.ts | 2 -- 11 files changed, 75 deletions(-) diff --git a/docs/content/docs/api.md b/docs/content/docs/api.md index ce7b20c..ceb9427 100644 --- a/docs/content/docs/api.md +++ b/docs/content/docs/api.md @@ -156,14 +156,6 @@ const lock1 = verrou.createLock('key', 'owner') const lock2 = verrou.restoreLock(lock1.serialize()) ``` -### `disconnect` - -Disconnect from the store if applicable. With a Redis Store, it will close the `ioredis` connection. - -```ts -await verrou.disconnect() -``` - ## Verrou API Verrou API is a wrapper around the LockFactory API. @@ -200,11 +192,3 @@ As explained above, the `use` method allows you to use a different store than th verrou.use('myMemoryStore').createLock('key') ``` -### `disconnectAll` - -Disconnect from all stores. - -```ts -await verrou.disconnectAll() -``` - diff --git a/docs/content/docs/extend/custom_lock_store.md b/docs/content/docs/extend/custom_lock_store.md index 70860d5..702e7ac 100644 --- a/docs/content/docs/extend/custom_lock_store.md +++ b/docs/content/docs/extend/custom_lock_store.md @@ -12,7 +12,6 @@ interface LockStore { delete(key: string, owner: string): Promise exists(key: string): Promise extend(key: string, owner: string, duration: number): Promise - disconnect(): Promise } ``` diff --git a/packages/verrou/src/drivers/database.ts b/packages/verrou/src/drivers/database.ts index 9e07fc0..c97dbc3 100644 --- a/packages/verrou/src/drivers/database.ts +++ b/packages/verrou/src/drivers/database.ts @@ -105,6 +105,4 @@ export class DatabaseStore { return lock.expiration > Date.now() } - - async disconnect() {} } diff --git a/packages/verrou/src/drivers/dynamodb.ts b/packages/verrou/src/drivers/dynamodb.ts index 6a9bbb5..d4080a4 100644 --- a/packages/verrou/src/drivers/dynamodb.ts +++ b/packages/verrou/src/drivers/dynamodb.ts @@ -166,11 +166,4 @@ export class DynamoDBStore implements LockStore { throw new E_LOCK_NOT_OWNED() } } - - /** - * Disconnect from DynamoDB - */ - async disconnect() { - this.#client.destroy() - } } diff --git a/packages/verrou/src/drivers/memory.ts b/packages/verrou/src/drivers/memory.ts index 2c99ea7..ecbf58c 100644 --- a/packages/verrou/src/drivers/memory.ts +++ b/packages/verrou/src/drivers/memory.ts @@ -109,8 +109,4 @@ export class MemoryStore implements LockStore { return lock.mutex.isLocked() } - - async disconnect() { - // noop - } } diff --git a/packages/verrou/src/drivers/redis.ts b/packages/verrou/src/drivers/redis.ts index efec8e8..1f8e1fa 100644 --- a/packages/verrou/src/drivers/redis.ts +++ b/packages/verrou/src/drivers/redis.ts @@ -85,11 +85,4 @@ export class RedisStore implements LockStore { const result = await this.#connection.eval(lua, 1, key, owner, duration) if (result === 0) throw new E_LOCK_NOT_OWNED() } - - /** - * Disconnect from Redis - */ - async disconnect() { - await this.#connection.quit() - } } diff --git a/packages/verrou/src/lock_factory.ts b/packages/verrou/src/lock_factory.ts index 7b7545b..d39966d 100644 --- a/packages/verrou/src/lock_factory.ts +++ b/packages/verrou/src/lock_factory.ts @@ -60,11 +60,4 @@ export class LockFactory { restoreLock(lock: SerializedLock) { return new Lock(lock.key, this.#store, this.#config, lock.owner, lock.ttl, lock.expirationTime) } - - /** - * Disconnect the store ( if applicable ) - */ - disconnect() { - return this.#store.disconnect() - } } diff --git a/packages/verrou/src/types/main.ts b/packages/verrou/src/types/main.ts index 478f791..126e45f 100644 --- a/packages/verrou/src/types/main.ts +++ b/packages/verrou/src/types/main.ts @@ -110,9 +110,4 @@ export interface LockStore { * Duration is in milliseconds */ extend(key: string, owner: string, duration: number): Promise - - /** - * Disconnect the store from the underlying storage ( when applicable ) - */ - disconnect(): Promise } diff --git a/packages/verrou/src/verrou.ts b/packages/verrou/src/verrou.ts index 4da81a4..08f810b 100644 --- a/packages/verrou/src/verrou.ts +++ b/packages/verrou/src/verrou.ts @@ -70,24 +70,4 @@ export class Verrou> { restoreLock(lock: SerializedLock) { return this.use(this.#defaultStoreName).restoreLock(lock) } - - /** - * Disconnect the default store - */ - async disconnect() { - await this.use(this.#defaultStoreName).disconnect() - } - - /** - * Disconnect all connections to the stores - */ - async disconnectAll() { - const promises = [] - - for (const store of this.#storesCache.values()) { - promises.push(store.disconnect()) - } - - await Promise.allSettled(promises) - } } diff --git a/packages/verrou/test_helpers/null_store.ts b/packages/verrou/test_helpers/null_store.ts index f099146..b038949 100644 --- a/packages/verrou/test_helpers/null_store.ts +++ b/packages/verrou/test_helpers/null_store.ts @@ -24,8 +24,4 @@ export class NullStore implements LockStore { async extend(_key: string, _owner: string, _duration: number): Promise { return } - - async disconnect() { - return - } } diff --git a/playground/src/index.ts b/playground/src/index.ts index 8599801..269fc47 100644 --- a/playground/src/index.ts +++ b/playground/src/index.ts @@ -69,5 +69,3 @@ await Promise.all([ purchaseProduct('123', 1, 'CustomerA'), purchaseProduct('123', 1, 'CustomerB'), ]) - -await verrou.disconnectAll() From 9808fc68b4cd02d9c679fa79a16f45d5931edda8 Mon Sep 17 00:00:00 2001 From: Julien Ripouteau Date: Tue, 12 Mar 2024 20:16:36 +0100 Subject: [PATCH 7/9] refactor: drivers only accept instance instead of creating it --- packages/verrou/src/drivers/dynamodb.ts | 25 ++++++++----------- packages/verrou/src/drivers/redis.ts | 8 ++---- packages/verrou/src/types/drivers.ts | 21 +++------------- .../verrou/tests/drivers/dynamodb.spec.ts | 13 ++++++---- 4 files changed, 24 insertions(+), 43 deletions(-) diff --git a/packages/verrou/src/drivers/dynamodb.ts b/packages/verrou/src/drivers/dynamodb.ts index d4080a4..0168825 100644 --- a/packages/verrou/src/drivers/dynamodb.ts +++ b/packages/verrou/src/drivers/dynamodb.ts @@ -1,8 +1,8 @@ +import type { DynamoDBClient } from '@aws-sdk/client-dynamodb' import { ConditionalCheckFailedException, CreateTableCommand, DeleteItemCommand, - DynamoDBClient, GetItemCommand, PutItemCommand, ResourceInUseException, @@ -22,9 +22,9 @@ export class DynamoDBStore implements LockStore { #initialized: Promise /** - * DynamoDB client + * DynamoDB connection */ - #client: DynamoDBClient + #connection: DynamoDBClient /** * DynamoDB table name @@ -33,12 +33,7 @@ export class DynamoDBStore implements LockStore { constructor(config: DynamoDbOptions) { this.#tableName = config.table.name - - this.#client = new DynamoDBClient({ - region: config.region, - credentials: config.credentials, - endpoint: config.endpoint, - }) + this.#connection = config.connection this.#initialized = this.#createTableIfNotExists() } @@ -58,7 +53,7 @@ export class DynamoDBStore implements LockStore { }) try { - await this.#client.send(command) + await this.#connection.send(command) } catch (error) { if (error instanceof ResourceInUseException) return throw error @@ -87,7 +82,7 @@ export class DynamoDBStore implements LockStore { ExpressionAttributeValues: { ':now': { N: Date.now().toString() } }, }) - const result = await this.#client.send(command) + const result = await this.#connection.send(command) return result.$metadata.httpStatusCode === 200 } catch (err) { if (err instanceof ConditionalCheckFailedException) return false @@ -109,7 +104,7 @@ export class DynamoDBStore implements LockStore { }) try { - await this.#client.send(command) + await this.#connection.send(command) } catch (err) { throw new E_LOCK_NOT_OWNED() } @@ -124,7 +119,7 @@ export class DynamoDBStore implements LockStore { Key: { key: { S: key } }, }) - await this.#client.send(command) + await this.#connection.send(command) } /** @@ -137,7 +132,7 @@ export class DynamoDBStore implements LockStore { Key: { key: { S: key } }, }) - const result = await this.#client.send(command) + const result = await this.#connection.send(command) const isExpired = result.Item?.expires_at?.N && result.Item.expires_at.N < Date.now().toString() return result.Item !== undefined && !isExpired @@ -161,7 +156,7 @@ export class DynamoDBStore implements LockStore { }) try { - await this.#client.send(command) + await this.#connection.send(command) } catch (err) { throw new E_LOCK_NOT_OWNED() } diff --git a/packages/verrou/src/drivers/redis.ts b/packages/verrou/src/drivers/redis.ts index 1f8e1fa..e25027f 100644 --- a/packages/verrou/src/drivers/redis.ts +++ b/packages/verrou/src/drivers/redis.ts @@ -1,4 +1,4 @@ -import { Redis as IoRedis } from 'ioredis' +import type { Redis as IoRedis } from 'ioredis' import { E_LOCK_NOT_OWNED } from '../errors.js' import type { LockStore, RedisStoreOptions } from '../types/main.js' @@ -17,11 +17,7 @@ export class RedisStore implements LockStore { #connection: IoRedis constructor(options: RedisStoreOptions) { - if (options.connection instanceof IoRedis) { - this.#connection = options.connection - } else { - this.#connection = new IoRedis(options.connection) - } + this.#connection = options.connection } /** diff --git a/packages/verrou/src/types/drivers.ts b/packages/verrou/src/types/drivers.ts index 2c4e938..233d882 100644 --- a/packages/verrou/src/types/drivers.ts +++ b/packages/verrou/src/types/drivers.ts @@ -1,7 +1,7 @@ import type { Knex } from 'knex' import type { Kysely } from 'kysely' -import type { DynamoDBClientConfig } from '@aws-sdk/client-dynamodb' -import type { RedisOptions as IoRedisOptions, Redis as IoRedis } from 'ioredis' +import type { Redis as IoRedis } from 'ioredis' +import type { DynamoDBClient } from '@aws-sdk/client-dynamodb' /** * Common options for database stores @@ -49,7 +49,7 @@ export type RedisStoreOptions = { /** * The Redis connection */ - connection: IoRedis | IoRedisOptions + connection: IoRedis } /** @@ -63,20 +63,7 @@ export type DynamoDbOptions = { name: string } - /** - * AWS credentials - */ - credentials?: DynamoDBClientConfig['credentials'] - - /** - * Region of your DynamoDB instance - */ - region: DynamoDBClientConfig['region'] - - /** - * Endpoint to your DynamoDB instance - */ - endpoint: DynamoDBClientConfig['endpoint'] + connection: DynamoDBClient } /** diff --git a/packages/verrou/tests/drivers/dynamodb.spec.ts b/packages/verrou/tests/drivers/dynamodb.spec.ts index 840e036..839e0f0 100644 --- a/packages/verrou/tests/drivers/dynamodb.spec.ts +++ b/packages/verrou/tests/drivers/dynamodb.spec.ts @@ -4,13 +4,13 @@ import { DeleteTableCommand, DynamoDBClient, GetItemCommand } from '@aws-sdk/cli import { DynamoDBStore } from '../../src/drivers/dynamodb.js' import { registerStoreTestSuite } from '../../src/test_suite.js' -const credentials = { +const dynamoClient = new DynamoDBClient({ region: 'eu-west-3', endpoint: process.env.DYNAMODB_ENDPOINT, credentials: { accessKeyId: 'foo', secretAccessKey: 'foo' }, -} -const config = { ...credentials, table: { name: 'verrou' } } -const dynamoClient = new DynamoDBClient(credentials) +}) + +const config = { connection: dynamoClient, table: { name: 'verrou' } } function deleteTableTeardown(tableName: string) { return async () => { @@ -22,7 +22,10 @@ function deleteTableTeardown(tableName: string) { test.group('DynamoDB Store', (group) => { group.each.teardown(deleteTableTeardown('verrou')) - registerStoreTestSuite({ test, createStore: () => new DynamoDBStore(config) }) + registerStoreTestSuite({ + test, + createStore: () => new DynamoDBStore(config), + }) test('should automatically create table', async ({ assert }) => { const store = new DynamoDBStore(config) From d34b727d07d86330655a760aabaa509b5f7dc7d5 Mon Sep 17 00:00:00 2001 From: Julien Ripouteau Date: Tue, 12 Mar 2024 20:20:51 +0100 Subject: [PATCH 8/9] refactor: update playground --- playground/package.json | 1 + playground/src/index.ts | 14 ++++++-------- pnpm-lock.yaml | 14 +++----------- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/playground/package.json b/playground/package.json index 66da898..dc5eba5 100644 --- a/playground/package.json +++ b/playground/package.json @@ -3,6 +3,7 @@ "type": "module", "dependencies": { "@verrou/core": "workspace:*", + "ioredis": "^5.3.2", "pino": "^8.17.2" }, "devDependencies": { diff --git a/playground/src/index.ts b/playground/src/index.ts index 269fc47..0f7c57c 100644 --- a/playground/src/index.ts +++ b/playground/src/index.ts @@ -1,4 +1,5 @@ import pino from 'pino' +import { Redis } from 'ioredis' import { Verrou } from '@verrou/core' import { setTimeout } from 'node:timers/promises' import { redisStore } from '@verrou/core/drivers/redis' @@ -8,18 +9,13 @@ const logger = pino.default({ level: 'debug', transport: { target: 'pino-pretty' logger.info('Hello world') +const ioredis = new Redis({ host: 'localhost', port: 6379 }) const verrou = new Verrou({ logger, default: 'redis', stores: { - memory: { - driver: memoryStore(), - }, - redis: { - driver: redisStore({ - connection: { host: 'localhost', port: 6379 }, - }), - }, + memory: { driver: memoryStore() }, + redis: { driver: redisStore({ connection: ioredis }) }, }, }) @@ -69,3 +65,5 @@ await Promise.all([ purchaseProduct('123', 1, 'CustomerA'), purchaseProduct('123', 1, 'CustomerB'), ]) + +await ioredis.quit() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b54b4a7..85fc9ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -196,6 +196,9 @@ importers: '@verrou/core': specifier: workspace:* version: link:../packages/verrou + ioredis: + specifier: ^5.3.2 + version: 5.3.2 pino: specifier: ^8.17.2 version: 8.17.2 @@ -1638,7 +1641,6 @@ packages: /@ioredis/commands@1.2.0: resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} - dev: true /@isaacs/cliui@8.0.2: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} @@ -3963,7 +3965,6 @@ packages: /cluster-key-slot@1.1.2: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} - dev: true /code-block-writer@12.0.0: resolution: {integrity: sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==} @@ -4257,7 +4258,6 @@ packages: optional: true dependencies: ms: 2.1.2 - dev: true /decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} @@ -4440,7 +4440,6 @@ packages: /denque@2.1.0: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} - dev: true /depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} @@ -6133,7 +6132,6 @@ packages: standard-as-callback: 2.1.0 transitivePeerDependencies: - supports-color - dev: true /ip@1.1.8: resolution: {integrity: sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==} @@ -6859,7 +6857,6 @@ packages: /lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} - dev: true /lodash.escaperegexp@4.1.2: resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} @@ -6875,7 +6872,6 @@ packages: /lodash.isarguments@3.1.0: resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} - dev: true /lodash.isequal@4.5.0: resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} @@ -7714,7 +7710,6 @@ packages: /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: true /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -8898,14 +8893,12 @@ packages: /redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} - dev: true /redis-parser@3.0.0: resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} engines: {node: '>=4'} dependencies: redis-errors: 1.2.0 - dev: true /reflect-metadata@0.2.1: resolution: {integrity: sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==} @@ -9610,7 +9603,6 @@ packages: /standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} - dev: true /statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} From 257971bed34a68b7c3ce0430404895083b5e83d9 Mon Sep 17 00:00:00 2001 From: Julien Ripouteau Date: Tue, 12 Mar 2024 20:33:08 +0100 Subject: [PATCH 9/9] doc: update documentation about drivers usage --- docs/content/docs/drivers.md | 225 ++++++++--------------------------- 1 file changed, 47 insertions(+), 178 deletions(-) diff --git a/docs/content/docs/drivers.md b/docs/content/docs/drivers.md index 2f930bd..2e45a81 100644 --- a/docs/content/docs/drivers.md +++ b/docs/content/docs/drivers.md @@ -22,16 +22,16 @@ The driver uses the [ioredis](https://github.com/redis/ioredis) library under th ```ts // title: Verrou API +import { Redis } from 'ioredis' import { Verrou } from '@verrou/core' import { redisStore } from '@verrou/core/drivers/redis' +const redis = new Redis({ host: 'localhost', port: 6379 }) const verrou = new Verrou({ default: 'redis', drivers: { - redis: { - driver: redisStore({ - connection: { host: 'localhost', port: 6379 } - }) + redis: { + driver: redisStore({ connection: redis }) }, } }) @@ -42,37 +42,19 @@ const verrou = new Verrou({ import { Verrou, LockFactory } from '@verrou/core' import { redisStore } from '@verrou/core/drivers/redis' -const store = redisStore({ - connection: { host: 'localhost', port: 6379 } -}) +const redis = new Redis({ host: 'localhost', port: 6379 }) +const store = redisStore({ connection: redis }) const lockFactory = new LockFactory(store) ``` ::: -It is also possible to directly pass an Ioredis instance to reuse a connection. - -```ts -import { Redis } from 'ioredis' -import { Verrou } from '@verrou/core' -import { redisStore } from '@verrou/core/drivers/redis' - -const ioredis = new Redis() - -const verrou = new Verrou({ - default: 'redis', - drivers: { - redis: { driver: redisStore({ connection: ioredis }) }, - } -}) -``` - ### Options | Option | Description | Default | | --- | --- | --- | -| `connection` | The connection options to use to connect to Redis or an instance of `ioredis` | N/A | +| `connection` | An instance of `ioredis` | N/A | ### Implementation details @@ -119,25 +101,18 @@ DynamoDB is also supported by Verrou. You will need to install `@aws-sdk/client- ```ts // title: Verrou API import { Verrou } from '@verrou/core' +import { DynamoDBClient } from '@aws-sdk/client-dynamodb' import { dynamodbStore } from '@verrou/core/drivers/dynamodb' +const dynamoClient = new DynamoDBClient(/* ... */) const verrou = new Verrou({ default: 'dynamo', stores: { dynamo: { driver: dynamodbStore({ - endpoint: '...', - region: 'eu-west-3', - table: { - // Name of the table where the locks will be stored - name: 'locks' - }, - - // Credentials to use to connect to DynamoDB - credentials: { - accessKeyId: '...', - secretAccessKey: '...' - } + connection: dynamoClient, + // Name of the table where the locks will be stored + table: { name: 'locks' }, }) } } @@ -147,21 +122,16 @@ const verrou = new Verrou({ ```ts // title: LockFactory API import { Verrou, LockFactory } from '@verrou/core' +import { DynamoDBClient } from '@aws-sdk/client-dynamodb' import { dynamodbStore } from '@verrou/core/drivers/dynamodb' +const dynamoClient = new DynamoDBClient(/* ... */) const store = dynamodbStore({ - endpoint: '...', - region: 'eu-west-3', + connection: dynamoClient, + // Name of the table where the locks will be stored table: { - // Name of the table where the locks will be stored name: 'locks' }, - - // Credentials to use to connect to DynamoDB - credentials: { - accessKeyId: '...', - secretAccessKey: '...' - } }) const lockFactory = new LockFactory(store) @@ -176,192 +146,91 @@ The DynamoDB table will be automatically created if it does not exists. Otherwis | Option | Description | Default | | --- | --- | --- | | `table.name` | The name of the table that will be used to store the cache. | `cache` | -| `credentials` | The credentials to use to connect to DynamoDB. | N/A | -| `endpoint` | The endpoint to use to connect to DynamoDB. | N/A | -| `region` | The region to use to connect to DynamoDB. | N/A | +| `connection` | An instance of `DynamoDBClient` | N/A | ## Databases -We offer several drivers to use a database as the locks store. Under the hood, we use [Knex](https://knexjs.org/). So all Knex options are available, feel free to check out the documentation. +We offer several drivers to use a database as the locks store. The database store should use an adapter for your database. Out of the box, we support [Knex](https://knexjs.org/) and [Kysely](https://kysely.dev/) to interact with the database. Knex and Kysely support many databases : SQLite, MySQL, PostgreSQL, MSSQL, Oracle, and more. + +:::note -All SQL drivers accept the following options: +Note that you can easily create your own adapter by implementing the `DatabaseAdapter` interface if you are using another library not supported by Verrou. See the [documentation](/docs/advanced/custom-adapters) for more details. + +::: + +All Database drivers accept the following common options: | Option | Description | Default | | --- | --- | --- | | `tableName` | The name of the table that will be used to store the locks. | `verrou` | | `autoCreateTable` | If the table should be automatically created if it does not exist. | `true` | -| `connection` | The connection options to use to connect to the database or an instance of `knex`. | N/A | -### PostgreSQL +### Knex -You will need to install `pg` to use this driver. +You must provide a Knex instance to use the Knex driver. Feel free to check the [Knex documentation](https://knexjs.org/) for more details about the configuration. Knex support many databases : SQLite, MySQL, PostgreSQL, MSSQL, Oracle, and more. :::codegroup ```ts // title: Verrou API +import knex from 'knex' import { Verrou } from '@verrou/core' -import { databaseStore } from '@verrou/core/drivers/database' +import { knexStore } from '@verrou/core/drivers/knex' -const verrou = new Verrou({ - default: 'pg', - stores: { - pg: { - driver: databaseStore({ - dialect: 'pg', - connection: { - user: 'root', - password: 'root', - database: 'postgres', - port: 5432 - } - }) - } - } -}) -``` - -```ts -// title: LockFactory API -import { Verrou, LockFactory } from '@verrou/core' -import { databaseStore } from '@verrou/core/drivers/database' - -const store = databaseStore({ - dialect: 'pg', - connection: { - user: 'root', - password: 'root', - database: 'postgres', - port: 5432 - } -}) -const lockFactory = new LockFactory(store) -``` - -::: - -### MySQL - -You will need to install `mysql2` to use this driver. - -:::codegroup - -```ts -// title: Verrou API -import { Verrou } from '@verrou/core' -import { databaseStore } from '@verrou/core/drivers/database' - -const verrou = new Verrou({ - default: 'mysql', - stores: { - mysql: { - driver: databaseStore({ - dialect: 'mysql', - connection: { - user: 'root', - password: 'root', - database: 'mysql', - port: 3306 - } - }) - } - } -}) -``` - -```ts -// title: LockFactory API -import { Verrou, LockFactory } from '@verrou/core' -import { databaseStore } from '@verrou/core/drivers/database' - -const store = databaseStore({ - dialect: 'mysql', - connection: { - user: 'root', - password: 'root', - database: 'mysql', - port: 3306 - } -}) - -const lockFactory = new LockFactory(store) -``` - -::: - -### SQLite ( better-sqlite3 ) - -You will need to install `better-sqlite3` to use this driver. - -:::codegroup - -```ts -// title: Verrou API -import { Verrou } from '@verrou/core' -import { databaseStore } from '@verrou/core/drivers/database' +const db = knex({ client: 'mysql2', connection: MYSQL_CREDENTIALS }) const verrou = new Verrou({ default: 'sqlite', stores: { - sqlite: { - driver: databaseStore({ - dialect: 'better-sqlite3', - connection: { filename: 'cache.sqlite3' } - }) - } + sqlite: { driver: knexStore({ connection: db }) } } }) ``` ```ts // title: LockFactory API +import knex from 'knex' import { Verrou, LockFactory } from '@verrou/core' -import { databaseStore } from '@verrou/core/drivers/database' - -const store = databaseStore({ - dialect: 'better-sqlite3', - connection: { filename: 'cache.sqlite3' } -}) +import { knexStore } from '@verrou/core/drivers/knex' +const db = knex({ client: 'mysql2', connection: MYSQL_CREDENTIALS }) +const store = knexStore({ dialect: 'sqlite', connection: db }) const lockFactory = new LockFactory(store) ``` ::: +### Kysely -### SQLite ( sqlite3 ) +You must provide a Kysely instance to use the Kysely driver. Feel free to check the [Kysely documentation](https://kysely.dev/) for more details about the configuration. Kysely support the following databases : SQLite, MySQL, PostgreSQL and MSSQL. -You will need to install `sqlite3` to use this driver. :::codegroup ```ts // title: Verrou API +import { Kysely } from 'kysely' import { Verrou } from '@verrou/core' -import { databaseStore } from '@verrou/core/drivers/database' +import { kyselyStore } from '@verrou/core/drivers/kysely' + +const db = new Kysely({ dialect }) const verrou = new Verrou({ - default: 'sqlite', + default: 'kysely', stores: { - sqlite: { - driver: databaseStore({ - connection: { filename: 'cache.sqlite3' } - }) - } + kysely: { driver: kyselyStore({ connection: db }) } } }) ``` ```ts // title: LockFactory API +import { Kysely } from 'kysely' import { Verrou, LockFactory } from '@verrou/core' -import { databaseStore } from '@verrou/core/drivers/database' - -const store = databaseStore({ - dialect: 'sqlite', - connection: { filename: 'cache.sqlite3' } -}) +import { kyselyStore } from '@verrou/core/drivers/kysely' +const db = new Kysely({ dialect }) +const store = kyselyStore({ connection: db }) const lockFactory = new LockFactory(store) ```