Merge commit '3c60637c1a27da8ba66888de518d58d5707801f2' as 'repos/effect-smol'
This commit is contained in:
3524
repos/effect-smol/packages/sql/sqlite-node/CHANGELOG.md
Normal file
3524
repos/effect-smol/packages/sql/sqlite-node/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
21
repos/effect-smol/packages/sql/sqlite-node/LICENSE
Normal file
21
repos/effect-smol/packages/sql/sqlite-node/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023-present The Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
7
repos/effect-smol/packages/sql/sqlite-node/README.md
Normal file
7
repos/effect-smol/packages/sql/sqlite-node/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# `@effect/sql-sqlite-node`
|
||||
|
||||
An `@effect/sql` implementation using Node.js' built-in `node:sqlite` module.
|
||||
|
||||
## Documentation
|
||||
|
||||
- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/sql-sqlite-node).
|
||||
23
repos/effect-smol/packages/sql/sqlite-node/docgen.json
Normal file
23
repos/effect-smol/packages/sql/sqlite-node/docgen.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "../../node_modules/@effect/docgen/schema.json",
|
||||
"srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/sql/sqlite-node/src/",
|
||||
"exclude": ["src/internal/**/*.ts"],
|
||||
"examplesCompilerOptions": {
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "Bundler",
|
||||
"module": "ES2022",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"rewriteRelativeImportExtensions": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"paths": {
|
||||
"effect": ["../../../effect/src/index.js"],
|
||||
"effect/*": ["../../../effect/src/*.js"]
|
||||
},
|
||||
"plugins": [
|
||||
{ "name": "@effect/language-service", "includeSuggestionsInTsc": false }
|
||||
]
|
||||
}
|
||||
}
|
||||
68
repos/effect-smol/packages/sql/sqlite-node/package.json
Normal file
68
repos/effect-smol/packages/sql/sqlite-node/package.json
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "@effect/sql-sqlite-node",
|
||||
"version": "4.0.0-beta.98",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "A SQLite toolkit for Effect",
|
||||
"homepage": "https://effect.website",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Effect-TS/effect-smol.git",
|
||||
"directory": "packages/sql/sqlite-node"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/Effect-TS/effect-smol/issues"
|
||||
},
|
||||
"tags": [
|
||||
"typescript",
|
||||
"sql",
|
||||
"database"
|
||||
],
|
||||
"keywords": [
|
||||
"typescript",
|
||||
"sql",
|
||||
"database"
|
||||
],
|
||||
"sideEffects": [],
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": "./src/index.ts",
|
||||
"./*": "./src/*.ts",
|
||||
"./internal/*": null,
|
||||
"./*/index": null
|
||||
},
|
||||
"files": [
|
||||
"src/**/*.ts",
|
||||
"dist/**/*.js",
|
||||
"dist/**/*.js.map",
|
||||
"dist/**/*.d.ts",
|
||||
"dist/**/*.d.ts.map"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"provenance": true,
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": "./dist/index.js",
|
||||
"./*": "./dist/*.js",
|
||||
"./internal/*": null,
|
||||
"./*/index": null
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"codegen": "effect-utils codegen",
|
||||
"build": "tsc -b tsconfig.json && pnpm babel",
|
||||
"babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps",
|
||||
"check": "tsc -b tsconfig.json",
|
||||
"test": "vitest",
|
||||
"coverage": "vitest --coverage"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/platform-node": "workspace:^",
|
||||
"@types/node": "^26.1.1",
|
||||
"effect": "workspace:^"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"effect": "workspace:^"
|
||||
}
|
||||
}
|
||||
371
repos/effect-smol/packages/sql/sqlite-node/src/SqliteClient.ts
Normal file
371
repos/effect-smol/packages/sql/sqlite-node/src/SqliteClient.ts
Normal file
@@ -0,0 +1,371 @@
|
||||
/**
|
||||
* Connects Effect SQL to SQLite on Node.js using `node:sqlite`.
|
||||
*
|
||||
* This module opens a SQLite database and exposes it as both `SqliteClient` and
|
||||
* the generic Effect SQL client. It serializes access through one connection,
|
||||
* caches prepared statements, enables WAL mode unless disabled, and supports
|
||||
* database backup, and extension loading. Streaming queries and
|
||||
* `updateValues` are not supported by this driver.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
import * as Cache from "effect/Cache"
|
||||
import * as Config from "effect/Config"
|
||||
import * as Context from "effect/Context"
|
||||
import * as Duration from "effect/Duration"
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Fiber from "effect/Fiber"
|
||||
import { identity } from "effect/Function"
|
||||
import * as Layer from "effect/Layer"
|
||||
import * as Scope from "effect/Scope"
|
||||
import * as Semaphore from "effect/Semaphore"
|
||||
import * as Stream from "effect/Stream"
|
||||
import * as Reactivity from "effect/unstable/reactivity/Reactivity"
|
||||
import * as Client from "effect/unstable/sql/SqlClient"
|
||||
import type { Connection } from "effect/unstable/sql/SqlConnection"
|
||||
import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError"
|
||||
import * as Statement from "effect/unstable/sql/Statement"
|
||||
import { backup as backupDatabase, DatabaseSync } from "node:sqlite"
|
||||
import type { StatementSync } from "node:sqlite"
|
||||
|
||||
const ATTR_DB_SYSTEM_NAME = "db.system.name"
|
||||
|
||||
/**
|
||||
* Runtime type identifier used to mark Node `SqliteClient` values.
|
||||
*
|
||||
* @category type IDs
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const TypeId: TypeId = "~@effect/sql-sqlite-node/SqliteClient"
|
||||
|
||||
/**
|
||||
* Type-level identifier used to mark Node `SqliteClient` values.
|
||||
*
|
||||
* @category type IDs
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export type TypeId = "~@effect/sql-sqlite-node/SqliteClient"
|
||||
|
||||
/**
|
||||
* Node SQLite client service, extending `SqlClient` with database export, backup, and extension loading helpers. `updateValues` is not supported.
|
||||
*
|
||||
* @category models
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export interface SqliteClient extends Client.SqlClient {
|
||||
readonly [TypeId]: TypeId
|
||||
readonly config: SqliteClientConfig
|
||||
readonly backup: (destination: string) => Effect.Effect<BackupMetadata, SqlError>
|
||||
readonly loadExtension: (path: string) => Effect.Effect<void, SqlError>
|
||||
|
||||
/** Not supported in sqlite */
|
||||
readonly updateValues: never
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata returned from a Node SQLite backup operation, reporting total and remaining page counts.
|
||||
*
|
||||
* @category models
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export interface BackupMetadata {
|
||||
readonly totalPages: number
|
||||
readonly remainingPages: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Service tag for the node SQLite client implementation.
|
||||
*
|
||||
* @category services
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const SqliteClient = Context.Service<SqliteClient>("@effect/sql-sqlite-node/SqliteClient")
|
||||
|
||||
/**
|
||||
* Configuration for a node SQLite client backed by `node:sqlite`, including the database filename, read-only mode, statement cache settings, WAL behavior, span attributes, and query/result name transforms.
|
||||
*
|
||||
* @category models
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export interface SqliteClientConfig {
|
||||
readonly filename: string
|
||||
readonly readonly?: boolean | undefined
|
||||
readonly prepareCacheSize?: number | undefined
|
||||
readonly prepareCacheTTL?: Duration.Input | undefined
|
||||
readonly disableWAL?: boolean | undefined
|
||||
readonly spanAttributes?: Record<string, unknown> | undefined
|
||||
|
||||
readonly transformResultNames?: ((str: string) => string) | undefined
|
||||
readonly transformQueryNames?: ((str: string) => string) | undefined
|
||||
}
|
||||
|
||||
interface SqliteConnection extends Connection {
|
||||
readonly backup: (destination: string) => Effect.Effect<BackupMetadata, SqlError>
|
||||
readonly loadExtension: (path: string) => Effect.Effect<void, SqlError>
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a scoped node SQLite client from the supplied configuration, using a single serialized connection with WAL enabled by default and exposing SQLite-specific `export`, `backup`, and `loadExtension` operations.
|
||||
*
|
||||
* @category constructors
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const make = (
|
||||
options: SqliteClientConfig
|
||||
): Effect.Effect<SqliteClient, never, Scope.Scope | Reactivity.Reactivity> =>
|
||||
Effect.gen(function*() {
|
||||
const compiler = Statement.makeCompilerSqlite(options.transformQueryNames)
|
||||
const transformRows = options.transformResultNames ?
|
||||
Statement.defaultTransforms(
|
||||
options.transformResultNames
|
||||
).array :
|
||||
undefined
|
||||
|
||||
const makeConnection = Effect.gen(function*() {
|
||||
const scope = yield* Effect.scope
|
||||
const db = new DatabaseSync(options.filename, {
|
||||
readOnly: options.readonly ?? false,
|
||||
allowExtension: true
|
||||
})
|
||||
yield* Scope.addFinalizer(scope, Effect.sync(() => db.close()))
|
||||
db.enableLoadExtension(false)
|
||||
|
||||
if (options.disableWAL !== true) {
|
||||
db.exec("PRAGMA journal_mode = WAL")
|
||||
}
|
||||
|
||||
const prepareCache = yield* Cache.make({
|
||||
capacity: options.prepareCacheSize ?? 200,
|
||||
timeToLive: options.prepareCacheTTL ?? Duration.minutes(10),
|
||||
lookup: (sql: string) =>
|
||||
Effect.try({
|
||||
try: () => db.prepare(sql),
|
||||
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to prepare statement", "prepare") })
|
||||
})
|
||||
})
|
||||
|
||||
const runStatement = (
|
||||
statement: StatementSync,
|
||||
params: ReadonlyArray<unknown>,
|
||||
raw: boolean
|
||||
) =>
|
||||
Effect.withFiber<ReadonlyArray<any>, SqlError>((fiber) => {
|
||||
const useSafeIntegers = Context.get(fiber.context, Client.SafeIntegers)
|
||||
return Effect.try({
|
||||
try: () => {
|
||||
statement.setReadBigInts(useSafeIntegers)
|
||||
if (statement.columns().length > 0) {
|
||||
return statement.all(...(params as Array<any>)) as ReadonlyArray<any>
|
||||
}
|
||||
const result = statement.run(...(params as Array<any>))
|
||||
return raw ? { changes: result.changes, lastInsertRowid: result.lastInsertRowid } as any : []
|
||||
},
|
||||
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") })
|
||||
})
|
||||
})
|
||||
|
||||
const runStatementValues = (
|
||||
statement: StatementSync,
|
||||
params: ReadonlyArray<unknown>
|
||||
) =>
|
||||
Effect.withFiber<ReadonlyArray<ReadonlyArray<unknown>>, SqlError>((fiber) => {
|
||||
const useSafeIntegers = Context.get(fiber.context, Client.SafeIntegers)
|
||||
return Effect.try({
|
||||
try: () => {
|
||||
statement.setReadBigInts(useSafeIntegers)
|
||||
if (statement.columns().length > 0) {
|
||||
return statement.all(...(params as Array<any>)) as unknown as ReadonlyArray<ReadonlyArray<unknown>>
|
||||
}
|
||||
statement.run(...(params as Array<any>))
|
||||
return []
|
||||
},
|
||||
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") })
|
||||
})
|
||||
})
|
||||
|
||||
const runStatementValuesUnprepared = (
|
||||
statement: StatementSync,
|
||||
params: ReadonlyArray<unknown>
|
||||
) =>
|
||||
Effect.withFiber<ReadonlyArray<ReadonlyArray<unknown>>, SqlError>((fiber) => {
|
||||
const useSafeIntegers = Context.get(fiber.context, Client.SafeIntegers)
|
||||
return Effect.try({
|
||||
try: () => {
|
||||
statement.setReadBigInts(useSafeIntegers)
|
||||
statement.setReturnArrays(true)
|
||||
if (statement.columns().length > 0) {
|
||||
return statement.all(...(params as Array<any>)) as unknown as ReadonlyArray<ReadonlyArray<unknown>>
|
||||
}
|
||||
statement.run(...(params as Array<any>))
|
||||
return []
|
||||
},
|
||||
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") })
|
||||
})
|
||||
})
|
||||
|
||||
const run = (
|
||||
sql: string,
|
||||
params: ReadonlyArray<unknown>,
|
||||
raw = false
|
||||
) =>
|
||||
Effect.flatMap(
|
||||
Cache.get(prepareCache, sql),
|
||||
(s) => runStatement(s, params, raw)
|
||||
)
|
||||
|
||||
const runValues = (
|
||||
sql: string,
|
||||
params: ReadonlyArray<unknown>
|
||||
) =>
|
||||
Effect.acquireUseRelease(
|
||||
Cache.get(prepareCache, sql),
|
||||
(statement) => {
|
||||
statement.setReturnArrays(true)
|
||||
return runStatementValues(statement, params)
|
||||
},
|
||||
(statement) => Effect.sync(() => statement.setReturnArrays(false))
|
||||
)
|
||||
|
||||
const runValuesUnprepared = (
|
||||
sql: string,
|
||||
params: ReadonlyArray<unknown>
|
||||
) => runStatementValuesUnprepared(db.prepare(sql), params)
|
||||
|
||||
return identity<SqliteConnection>({
|
||||
execute(sql, params, transformRows) {
|
||||
return transformRows
|
||||
? Effect.map(run(sql, params), transformRows)
|
||||
: run(sql, params)
|
||||
},
|
||||
executeRaw(sql, params) {
|
||||
return run(sql, params, true)
|
||||
},
|
||||
executeValues(sql, params) {
|
||||
return runValues(sql, params)
|
||||
},
|
||||
executeValuesUnprepared(sql, params) {
|
||||
return runValuesUnprepared(sql, params)
|
||||
},
|
||||
executeUnprepared(sql, params, transformRows) {
|
||||
const effect = runStatement(db.prepare(sql), params ?? [], false)
|
||||
return transformRows ? Effect.map(effect, transformRows) : effect
|
||||
},
|
||||
executeStream(_sql, _params) {
|
||||
return Stream.die("executeStream not implemented")
|
||||
},
|
||||
backup(destination) {
|
||||
return Effect.suspend(() => {
|
||||
let totalPages = 0
|
||||
return Effect.tryPromise({
|
||||
try: () =>
|
||||
backupDatabase(db, destination, {
|
||||
progress: (progress) => {
|
||||
totalPages = progress.totalPages
|
||||
}
|
||||
}).then((pages): BackupMetadata => ({ totalPages: totalPages || pages, remainingPages: 0 })),
|
||||
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to backup database", "backup") })
|
||||
})
|
||||
})
|
||||
},
|
||||
loadExtension(path) {
|
||||
return Effect.acquireUseRelease(
|
||||
Effect.sync(() => db.enableLoadExtension(true)),
|
||||
() =>
|
||||
Effect.try({
|
||||
try: () => db.loadExtension(path),
|
||||
catch: (cause) =>
|
||||
new SqlError({ reason: classifyError(cause, "Failed to load extension", "loadExtension") })
|
||||
}),
|
||||
() => Effect.sync(() => db.enableLoadExtension(false))
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const semaphore = yield* Semaphore.make(1)
|
||||
const connection = yield* makeConnection
|
||||
|
||||
const acquirer = semaphore.withPermits(1)(Effect.succeed(connection))
|
||||
const transactionAcquirer = Effect.uninterruptibleMask((restore) => {
|
||||
const fiber = Fiber.getCurrent()!
|
||||
const scope = Context.getUnsafe(fiber.context, Scope.Scope)
|
||||
return Effect.as(
|
||||
Effect.tap(
|
||||
restore(semaphore.take(1)),
|
||||
() => Scope.addFinalizer(scope, semaphore.release(1))
|
||||
),
|
||||
connection
|
||||
)
|
||||
})
|
||||
|
||||
return Object.assign(
|
||||
(yield* Client.make({
|
||||
acquirer,
|
||||
compiler,
|
||||
transactionAcquirer,
|
||||
spanAttributes: [
|
||||
...(options.spanAttributes ? Object.entries(options.spanAttributes) : []),
|
||||
[ATTR_DB_SYSTEM_NAME, "sqlite"]
|
||||
],
|
||||
transformRows
|
||||
})) as SqliteClient,
|
||||
{
|
||||
[TypeId]: TypeId as TypeId,
|
||||
config: options,
|
||||
backup: (destination: string) => Effect.flatMap(acquirer, (_) => _.backup(destination)),
|
||||
loadExtension: (path: string) => Effect.flatMap(acquirer, (_) => _.loadExtension(path))
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Builds a layer from an Effect `Config` value, providing both the node `SqliteClient` service and the generic `SqlClient` service.
|
||||
*
|
||||
* @category layers
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const layerConfig = (
|
||||
config: Config.Wrap<SqliteClientConfig>
|
||||
): Layer.Layer<SqliteClient | Client.SqlClient, Config.ConfigError> =>
|
||||
Layer.effectContext(
|
||||
Config.unwrap(config).pipe(
|
||||
Effect.flatMap(make),
|
||||
Effect.map((client) =>
|
||||
Context.make(SqliteClient, client).pipe(
|
||||
Context.add(Client.SqlClient, client)
|
||||
)
|
||||
)
|
||||
)
|
||||
).pipe(Layer.provide(Reactivity.layer))
|
||||
|
||||
/**
|
||||
* Builds a layer from a node SQLite client configuration, providing both `SqliteClient` and the generic `SqlClient` service.
|
||||
*
|
||||
* @category layers
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const layer = (
|
||||
config: SqliteClientConfig
|
||||
): Layer.Layer<SqliteClient | Client.SqlClient> =>
|
||||
Layer.effectContext(
|
||||
Effect.map(make(config), (client) =>
|
||||
Context.make(SqliteClient, client).pipe(
|
||||
Context.add(Client.SqlClient, client)
|
||||
))
|
||||
).pipe(Layer.provide(Reactivity.layer))
|
||||
|
||||
// internal
|
||||
|
||||
const classifyError = (cause: unknown, message: string, operation: string) =>
|
||||
classifySqliteError(sqliteCauseWithErrno(cause), { message, operation })
|
||||
|
||||
const sqliteCauseWithErrno = (cause: unknown): unknown => {
|
||||
if (typeof cause !== "object" || cause === null || !("errcode" in cause) || "errno" in cause) {
|
||||
return cause
|
||||
}
|
||||
const errcode = (cause as { readonly errcode: unknown }).errcode
|
||||
if (typeof errcode !== "number") {
|
||||
return cause
|
||||
}
|
||||
return Object.assign(cause, { errno: errcode })
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Runs database migrations for Node.js SQLite projects that use Effect SQL.
|
||||
*
|
||||
* This module re-exports the shared migration loaders and errors, then provides
|
||||
* `run` and `layer` helpers that apply pending migration files with the current
|
||||
* `SqlClient`. It does not add Node-specific schema dump support; migration
|
||||
* execution is handled by the shared SQL migrator.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
import type * as Effect from "effect/Effect"
|
||||
import * as Layer from "effect/Layer"
|
||||
import * as Migrator from "effect/unstable/sql/Migrator"
|
||||
import type * as Client from "effect/unstable/sql/SqlClient"
|
||||
import type { SqlError } from "effect/unstable/sql/SqlError"
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export * from "effect/unstable/sql/Migrator"
|
||||
|
||||
/**
|
||||
* Runs SQL migrations for a SQLite database using the shared `Migrator` implementation and the current `SqlClient`.
|
||||
*
|
||||
* @category constructors
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const run: <R2 = never>(
|
||||
options: Migrator.MigratorOptions<R2>
|
||||
) => Effect.Effect<
|
||||
ReadonlyArray<readonly [id: number, name: string]>,
|
||||
Migrator.MigrationError | SqlError,
|
||||
Client.SqlClient | R2
|
||||
> = Migrator.make({
|
||||
// dumpSchema(path, table) {
|
||||
// const dump = (args: Array<string>) =>
|
||||
// Effect.gen(function*() {
|
||||
// const sql = yield* SqliteClient
|
||||
// const dump = yield* pipe(
|
||||
// Command.make("sqlite3", sql.config.filename, ...args),
|
||||
// Command.string
|
||||
// )
|
||||
// return dump.replace(/^create table sqlite_sequence\(.*$/im, "")
|
||||
// .replace(/\n{2,}/gm, "\n\n")
|
||||
// .trim()
|
||||
// }).pipe(
|
||||
// Effect.mapError((error) => new Migrator.MigrationError({ kind: "Failed", message: error.message }))
|
||||
// )
|
||||
//
|
||||
// const dumpSchema = dump([".schema"])
|
||||
//
|
||||
// const dumpMigrations = dump([
|
||||
// "--cmd",
|
||||
// `.mode insert ${table}`,
|
||||
// `select * from ${table}`
|
||||
// ])
|
||||
//
|
||||
// const dumpAll = Effect.map(
|
||||
// Effect.all([dumpSchema, dumpMigrations], { concurrency: 2 }),
|
||||
// ([schema, migrations]) => schema + "\n\n" + migrations
|
||||
// )
|
||||
//
|
||||
// const dumpFile = (file: string) =>
|
||||
// Effect.gen(function*() {
|
||||
// const fs = yield* FileSystem
|
||||
// const path = yield* Path
|
||||
// const dump = yield* dumpAll
|
||||
// yield* fs.makeDirectory(path.dirname(file), { recursive: true })
|
||||
// yield* fs.writeFileString(file, dump)
|
||||
// }).pipe(
|
||||
// Effect.mapError((error) => new Migrator.MigrationError({ kind: "Failed", message: error.message }))
|
||||
// )
|
||||
//
|
||||
// return dumpFile(path)
|
||||
// }
|
||||
})
|
||||
|
||||
/**
|
||||
* Creates a layer that runs the configured SQLite migrations during layer construction and provides no services.
|
||||
*
|
||||
* @category constructors
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const layer = <R>(
|
||||
options: Migrator.MigratorOptions<R>
|
||||
): Layer.Layer<
|
||||
never,
|
||||
Migrator.MigrationError | SqlError,
|
||||
Client.SqlClient | R
|
||||
> => Layer.effectDiscard(run(options))
|
||||
15
repos/effect-smol/packages/sql/sqlite-node/src/index.ts
Normal file
15
repos/effect-smol/packages/sql/sqlite-node/src/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
|
||||
// @barrel: Auto-generated exports. Do not edit manually.
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export * as SqliteClient from "./SqliteClient.ts"
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export * as SqliteMigrator from "./SqliteMigrator.ts"
|
||||
@@ -0,0 +1,88 @@
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { SqliteClient } from "@effect/sql-sqlite-node"
|
||||
import { assert, describe, it } from "@effect/vitest"
|
||||
import { Effect, FileSystem } from "effect"
|
||||
import { Reactivity } from "effect/unstable/reactivity"
|
||||
|
||||
const makeClient = Effect.gen(function*() {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const dir = yield* fs.makeTempDirectoryScoped()
|
||||
return yield* SqliteClient.make({
|
||||
filename: dir + "/test.db"
|
||||
})
|
||||
}).pipe(Effect.provide([NodeFileSystem.layer, Reactivity.layer]))
|
||||
|
||||
describe("Client", () => {
|
||||
it.effect("should work", () =>
|
||||
Effect.gen(function*() {
|
||||
const sql = yield* makeClient
|
||||
let response
|
||||
response = yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)`
|
||||
assert.deepStrictEqual(response, [])
|
||||
response = yield* sql`INSERT INTO test (name) VALUES ('hello')`
|
||||
assert.deepStrictEqual(response, [])
|
||||
response = yield* sql`SELECT * FROM test`
|
||||
assert.deepStrictEqual(response, [{ id: 1, name: "hello" }])
|
||||
response = yield* sql`SELECT * FROM test`.valuesUnprepared
|
||||
assert.deepStrictEqual(response, [[1, "hello"]])
|
||||
response = yield* sql`INSERT INTO test (name) VALUES ('world')`.pipe(sql.withTransaction)
|
||||
assert.deepStrictEqual(response, [])
|
||||
response = yield* sql`SELECT * FROM test`
|
||||
assert.deepStrictEqual(response, [
|
||||
{ id: 1, name: "hello" },
|
||||
{ id: 2, name: "world" }
|
||||
])
|
||||
}))
|
||||
|
||||
it.effect("should work with raw", () =>
|
||||
Effect.gen(function*() {
|
||||
const sql = yield* makeClient
|
||||
let response
|
||||
response = yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)`.raw
|
||||
assert.deepStrictEqual(response, { changes: 0, lastInsertRowid: 0 })
|
||||
response = yield* sql`INSERT INTO test (name) VALUES ('hello')`.raw
|
||||
assert.deepStrictEqual(response, { changes: 1, lastInsertRowid: 1 })
|
||||
response = yield* sql`SELECT * FROM test`.raw
|
||||
assert.deepStrictEqual(response, [{ id: 1, name: "hello" }])
|
||||
response = yield* sql`INSERT INTO test (name) VALUES ('world')`.raw.pipe(sql.withTransaction)
|
||||
assert.deepStrictEqual(response, { changes: 1, lastInsertRowid: 2 })
|
||||
response = yield* sql`SELECT * FROM test`
|
||||
assert.deepStrictEqual(response, [
|
||||
{ id: 1, name: "hello" },
|
||||
{ id: 2, name: "world" }
|
||||
])
|
||||
}))
|
||||
|
||||
it.effect("withTransaction", () =>
|
||||
Effect.gen(function*() {
|
||||
const sql = yield* makeClient
|
||||
yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)`
|
||||
yield* sql.withTransaction(sql`INSERT INTO test (name) VALUES ('hello')`)
|
||||
const rows = yield* sql`SELECT * FROM test`
|
||||
assert.deepStrictEqual(rows, [{ id: 1, name: "hello" }])
|
||||
}))
|
||||
|
||||
it.effect("withTransaction rollback", () =>
|
||||
Effect.gen(function*() {
|
||||
const sql = yield* makeClient
|
||||
yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)`
|
||||
yield* sql`INSERT INTO test (name) VALUES ('hello')`.pipe(
|
||||
Effect.andThen(Effect.fail("boom")),
|
||||
sql.withTransaction,
|
||||
Effect.ignore
|
||||
)
|
||||
const rows = yield* sql`SELECT * FROM test`
|
||||
assert.deepStrictEqual(rows, [])
|
||||
}))
|
||||
|
||||
it.effect("supports backup and export", () =>
|
||||
Effect.gen(function*() {
|
||||
const sql = yield* makeClient
|
||||
yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)`
|
||||
yield* sql`INSERT INTO test (name) VALUES ('hello')`
|
||||
|
||||
const metadata = yield* sql.backup(sql.config.filename + ".backup")
|
||||
assert(metadata.totalPages > 0)
|
||||
assert.strictEqual(metadata.remainingPages, 0)
|
||||
}))
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { SqliteClient } from "@effect/sql-sqlite-node"
|
||||
import { Effect, FileSystem, Layer } from "effect"
|
||||
import * as KeyValueStoreTest from "effect-test/unstable/persistence/KeyValueStoreTest"
|
||||
import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore"
|
||||
import { Reactivity } from "effect/unstable/reactivity"
|
||||
|
||||
const ClientLayer = Effect.gen(function*() {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const dir = yield* fs.makeTempDirectoryScoped()
|
||||
return SqliteClient.layer({
|
||||
filename: dir + "/test.db"
|
||||
})
|
||||
}).pipe(
|
||||
Layer.unwrap,
|
||||
Layer.provide([NodeFileSystem.layer, Reactivity.layer])
|
||||
)
|
||||
|
||||
KeyValueStoreTest.suite(
|
||||
"sql-sqlite",
|
||||
KeyValueStore.layerSql().pipe(Layer.provide(ClientLayer))
|
||||
)
|
||||
|
||||
KeyValueStoreTest.suite(
|
||||
"sql-sqlite-custom-table",
|
||||
KeyValueStore.layerSql({ table: "effect_key_value_store_custom" }).pipe(
|
||||
Layer.provide(ClientLayer)
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,109 @@
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { SqliteClient } from "@effect/sql-sqlite-node"
|
||||
import { expect, it } from "@effect/vitest"
|
||||
import { Duration, Effect, FileSystem, Layer } from "effect"
|
||||
import { TestClock } from "effect/testing"
|
||||
import { Persistence } from "effect/unstable/persistence"
|
||||
import { Reactivity } from "effect/unstable/reactivity"
|
||||
import type * as SqlClient from "effect/unstable/sql/SqlClient"
|
||||
|
||||
const ClientLayer = Effect.gen(function*() {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const dir = yield* fs.makeTempDirectoryScoped()
|
||||
return SqliteClient.layer({
|
||||
filename: dir + "/test.db"
|
||||
})
|
||||
}).pipe(
|
||||
Layer.unwrap,
|
||||
Layer.provide([NodeFileSystem.layer, Reactivity.layer])
|
||||
)
|
||||
|
||||
const testLayer = (layer: Layer.Layer<Persistence.BackingPersistence, never, SqlClient.SqlClient>) =>
|
||||
layer.pipe(Layer.provideMerge(ClientLayer))
|
||||
|
||||
const suite = (name: string, layer: Layer.Layer<Persistence.BackingPersistence, never, SqlClient.SqlClient>) =>
|
||||
it.layer(testLayer(layer))(`Persistence (${name})`, (it) => {
|
||||
it.effect("set + get", () =>
|
||||
Effect.gen(function*() {
|
||||
const persistence = yield* Persistence.BackingPersistence
|
||||
const store = yield* persistence.make("test_store")
|
||||
yield* store.set("key1", { name: "Alice" }, undefined)
|
||||
const value = yield* store.get("key1")
|
||||
expect(value).toEqual({ name: "Alice" })
|
||||
|
||||
// test upsert
|
||||
yield* store.set("key1", { name: "Alice" }, undefined)
|
||||
}))
|
||||
|
||||
it.effect("setMany + getMany", () =>
|
||||
Effect.gen(function*() {
|
||||
const persistence = yield* Persistence.BackingPersistence
|
||||
const store = yield* persistence.make("test_store_2")
|
||||
yield* store.setMany([
|
||||
["key1", { name: "Alice" }, undefined],
|
||||
["key2", { name: "Bob" }, undefined],
|
||||
["key3", { name: "Charlie" }, undefined]
|
||||
])
|
||||
const values = yield* store.getMany(["key1", "key2", "key3", "key4"])
|
||||
expect(values).toEqual([
|
||||
{ name: "Alice" },
|
||||
{ name: "Bob" },
|
||||
{ name: "Charlie" },
|
||||
undefined
|
||||
])
|
||||
}))
|
||||
|
||||
it.effect("remove", () =>
|
||||
Effect.gen(function*() {
|
||||
const persistence = yield* Persistence.BackingPersistence
|
||||
const store = yield* persistence.make("test_store_2")
|
||||
yield* store.setMany([
|
||||
["key1", { name: "Alice" }, undefined],
|
||||
["key2", { name: "Bob" }, undefined],
|
||||
["key3", { name: "Charlie" }, undefined]
|
||||
])
|
||||
yield* store.remove("key2")
|
||||
const valuesAfter = yield* store.getMany(["key1", "key2", "key3"])
|
||||
expect(valuesAfter).toEqual([{ name: "Alice" }, undefined, { name: "Charlie" }])
|
||||
}))
|
||||
|
||||
it.effect("expires", () =>
|
||||
Effect.gen(function*() {
|
||||
const persistence = yield* Persistence.BackingPersistence
|
||||
const store = yield* persistence.make("test_store_3")
|
||||
yield* store.setMany([
|
||||
["key1", { name: "Alice" }, undefined],
|
||||
["key2", { name: "Bob" }, undefined],
|
||||
["key3", { name: "Charlie" }, Duration.seconds(10)]
|
||||
])
|
||||
let values = yield* store.getMany(["key1", "key2", "key3"])
|
||||
expect(values).toEqual([{ name: "Alice" }, { name: "Bob" }, { name: "Charlie" }])
|
||||
yield* TestClock.adjust(Duration.seconds(5))
|
||||
values = yield* store.getMany(["key1", "key2", "key3"])
|
||||
expect(values).toEqual([{ name: "Alice" }, { name: "Bob" }, { name: "Charlie" }])
|
||||
yield* TestClock.adjust(Duration.seconds(5))
|
||||
values = yield* store.getMany(["key1", "key2", "key3"])
|
||||
expect(values).toEqual([{ name: "Alice" }, { name: "Bob" }, undefined])
|
||||
}))
|
||||
|
||||
it.effect("isolation between stores", () =>
|
||||
Effect.gen(function*() {
|
||||
const persistence = yield* Persistence.BackingPersistence
|
||||
const storeA = yield* persistence.make("test_store_a")
|
||||
const storeB = yield* persistence.make("test_store_b")
|
||||
|
||||
yield* storeA.set("shared-key", { name: "Alice" }, undefined)
|
||||
yield* storeB.set("shared-key", { name: "Bob" }, undefined)
|
||||
|
||||
expect(yield* storeA.get("shared-key")).toEqual({ name: "Alice" })
|
||||
expect(yield* storeB.get("shared-key")).toEqual({ name: "Bob" })
|
||||
|
||||
yield* storeA.clear
|
||||
|
||||
expect(yield* storeA.get("shared-key")).toEqual(undefined)
|
||||
expect(yield* storeB.get("shared-key")).toEqual({ name: "Bob" })
|
||||
}))
|
||||
})
|
||||
|
||||
suite("table-per-store", Persistence.layerBackingSqlMultiTable)
|
||||
suite("single-table", Persistence.layerBackingSql)
|
||||
183
repos/effect-smol/packages/sql/sqlite-node/test/Resolver.test.ts
Normal file
183
repos/effect-smol/packages/sql/sqlite-node/test/Resolver.test.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { SqliteClient } from "@effect/sql-sqlite-node"
|
||||
import { assert, describe, it } from "@effect/vitest"
|
||||
import { Cause, Effect, FileSystem, Iterable, Schema } from "effect"
|
||||
import { Reactivity } from "effect/unstable/reactivity"
|
||||
import { SqlError, SqlResolver } from "effect/unstable/sql"
|
||||
|
||||
const makeClient = Effect.gen(function*() {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const dir = yield* fs.makeTempDirectoryScoped()
|
||||
return yield* SqliteClient.make({
|
||||
filename: dir + "/test.db"
|
||||
})
|
||||
}).pipe(Effect.provide([NodeFileSystem.layer, Reactivity.layer]))
|
||||
|
||||
const seededClient = Effect.gen(function*() {
|
||||
const sql = yield* makeClient
|
||||
yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)`
|
||||
for (const id of Iterable.range(1, 100)) {
|
||||
yield* sql`INSERT INTO test ${sql.insert({ id, name: `name${id}` })}`
|
||||
}
|
||||
return sql
|
||||
})
|
||||
|
||||
describe("Resolver", () => {
|
||||
describe("ordered", () => {
|
||||
it.effect("insert", () =>
|
||||
Effect.gen(function*() {
|
||||
const batches: Array<Array<string>> = []
|
||||
const sql = yield* seededClient
|
||||
const Insert = SqlResolver.ordered({
|
||||
Request: Schema.String,
|
||||
Result: Schema.Struct({ id: Schema.Number, name: Schema.String }),
|
||||
execute: (names) => {
|
||||
batches.push(names)
|
||||
return sql`INSERT INTO test ${sql.insert(names.map((name) => ({ name })))} RETURNING *`
|
||||
}
|
||||
})
|
||||
const execute = SqlResolver.request(Insert)
|
||||
assert.deepStrictEqual(
|
||||
yield* Effect.all({
|
||||
one: execute("one"),
|
||||
two: execute("two")
|
||||
}, { concurrency: "unbounded" }),
|
||||
{
|
||||
one: { id: 101, name: "one" },
|
||||
two: { id: 102, name: "two" }
|
||||
}
|
||||
)
|
||||
assert.deepStrictEqual(batches, [["one", "two"]])
|
||||
}))
|
||||
|
||||
it.effect("result length mismatch", () =>
|
||||
Effect.gen(function*() {
|
||||
const batches: Array<Array<number>> = []
|
||||
const sql = yield* seededClient
|
||||
const Select = SqlResolver.ordered({
|
||||
Request: Schema.Number,
|
||||
Result: Schema.Struct({ id: Schema.Number, name: Schema.String }),
|
||||
execute: (ids) => {
|
||||
batches.push(ids)
|
||||
return sql`SELECT * FROM test WHERE id IN ${sql.in(ids)}`
|
||||
}
|
||||
})
|
||||
const execute = SqlResolver.request(Select)
|
||||
const error = yield* Effect.all([
|
||||
execute(1),
|
||||
execute(2),
|
||||
execute(3),
|
||||
execute(101)
|
||||
], { concurrency: "unbounded" })
|
||||
.pipe(Effect.flip)
|
||||
assert(error instanceof SqlError.ResultLengthMismatch)
|
||||
assert.strictEqual(error.actual, 3)
|
||||
assert.strictEqual(error.expected, 4)
|
||||
assert.deepStrictEqual(batches, [[1, 2, 3, 101]])
|
||||
}))
|
||||
})
|
||||
|
||||
describe("grouped", () => {
|
||||
it.effect("find by name", () =>
|
||||
Effect.gen(function*() {
|
||||
const sql = yield* seededClient
|
||||
const FindByName = SqlResolver.grouped({
|
||||
Request: Schema.String,
|
||||
RequestGroupKey: (name) => name,
|
||||
Result: Schema.Struct({ id: Schema.Number, name: Schema.String }),
|
||||
ResultGroupKey: (result) => result.name,
|
||||
execute: (names) => sql`SELECT * FROM test WHERE name IN ${sql.in(names)}`
|
||||
})
|
||||
yield* sql`INSERT INTO test ${sql.insert({ name: "name1" })}`
|
||||
const execute = SqlResolver.request(FindByName)
|
||||
assert.deepStrictEqual(
|
||||
yield* Effect.all({
|
||||
one: execute("name1"),
|
||||
two: execute("name2"),
|
||||
three: Effect.flip(execute("name0"))
|
||||
}, { concurrency: "unbounded" }),
|
||||
{
|
||||
one: [{ id: 1, name: "name1" }, { id: 101, name: "name1" }],
|
||||
two: [{ id: 2, name: "name2" }],
|
||||
three: new Cause.NoSuchElementError()
|
||||
}
|
||||
)
|
||||
}))
|
||||
|
||||
it.effect("using raw rows", () =>
|
||||
Effect.gen(function*() {
|
||||
const sql = yield* seededClient
|
||||
const FindByName = SqlResolver.grouped({
|
||||
Request: Schema.String,
|
||||
RequestGroupKey: (name) => name,
|
||||
Result: Schema.Struct({ id: Schema.Number, name: Schema.String }),
|
||||
ResultGroupKey: (_, result: any) => result.name,
|
||||
execute: (names) => sql`SELECT * FROM test WHERE name IN ${sql.in(names)}`
|
||||
})
|
||||
yield* sql`INSERT INTO test ${sql.insert({ name: "name1" })}`
|
||||
const execute = SqlResolver.request(FindByName)
|
||||
assert.deepStrictEqual(
|
||||
yield* Effect.all({
|
||||
one: execute("name1"),
|
||||
two: execute("name2"),
|
||||
three: Effect.flip(execute("name0"))
|
||||
}, { concurrency: "unbounded" }),
|
||||
{
|
||||
one: [{ id: 1, name: "name1" }, { id: 101, name: "name1" }],
|
||||
two: [{ id: 2, name: "name2" }],
|
||||
three: new Cause.NoSuchElementError()
|
||||
}
|
||||
)
|
||||
}))
|
||||
})
|
||||
|
||||
describe("findById", () => {
|
||||
it.effect("find by id", () =>
|
||||
Effect.gen(function*() {
|
||||
const sql = yield* seededClient
|
||||
const FindById = SqlResolver.findById({
|
||||
Id: Schema.Number,
|
||||
Result: Schema.Struct({ id: Schema.Number, name: Schema.String }),
|
||||
ResultId: (result) => result.id,
|
||||
execute: (ids) => sql`SELECT * FROM test WHERE id IN ${sql.in(ids)}`
|
||||
})
|
||||
const execute = SqlResolver.request(FindById)
|
||||
assert.deepStrictEqual(
|
||||
yield* Effect.all({
|
||||
one: execute(1),
|
||||
two: execute(2),
|
||||
three: Effect.flip(execute(101))
|
||||
}, { concurrency: "unbounded" }),
|
||||
{
|
||||
one: { id: 1, name: "name1" },
|
||||
two: { id: 2, name: "name2" },
|
||||
three: new Cause.NoSuchElementError()
|
||||
}
|
||||
)
|
||||
}))
|
||||
|
||||
it.effect("using raw rows", () =>
|
||||
Effect.gen(function*() {
|
||||
const sql = yield* seededClient
|
||||
const FindById = SqlResolver.findById({
|
||||
Id: Schema.Number,
|
||||
Result: Schema.Struct({ id: Schema.Number, name: Schema.String }),
|
||||
ResultId: (_, result: any) => result.id,
|
||||
execute: (ids) => sql`SELECT * FROM test WHERE id IN ${sql.in(ids)}`
|
||||
})
|
||||
const execute = SqlResolver.request(FindById)
|
||||
assert.deepStrictEqual(
|
||||
yield* Effect.all({
|
||||
one: execute(1),
|
||||
two: execute(2),
|
||||
three: Effect.flip(execute(101))
|
||||
}, { concurrency: "unbounded" }),
|
||||
{
|
||||
one: { id: 1, name: "name1" },
|
||||
two: { id: 2, name: "name2" },
|
||||
three: new Cause.NoSuchElementError()
|
||||
}
|
||||
)
|
||||
}))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,84 @@
|
||||
import { SqliteClient } from "@effect/sql-sqlite-node"
|
||||
import { assert, describe, it } from "@effect/vitest"
|
||||
import { Effect } from "effect"
|
||||
import * as EventJournal from "effect/unstable/eventlog/EventJournal"
|
||||
import * as SqlEventJournal from "effect/unstable/eventlog/SqlEventJournal"
|
||||
import { Reactivity } from "effect/unstable/reactivity"
|
||||
import * as SqlClient from "effect/unstable/sql/SqlClient"
|
||||
|
||||
const makeJournal = Effect.gen(function*() {
|
||||
const sql = yield* SqliteClient.make({ filename: ":memory:" })
|
||||
return yield* SqlEventJournal.make().pipe(
|
||||
Effect.provideService(SqlClient.SqlClient, sql)
|
||||
)
|
||||
}).pipe(Effect.provide(Reactivity.layer))
|
||||
|
||||
describe("SqlEventJournal", () => {
|
||||
it.effect("writes and reads entries", () =>
|
||||
Effect.gen(function*() {
|
||||
const journal = yield* makeJournal
|
||||
let createdAt = 0
|
||||
yield* journal.write({
|
||||
event: "UserCreated",
|
||||
primaryKey: "user-1",
|
||||
payload: new Uint8Array([1]),
|
||||
effect: (entry) =>
|
||||
Effect.sync(() => {
|
||||
createdAt = entry.createdAtMillis
|
||||
})
|
||||
})
|
||||
const entries = yield* journal.entries
|
||||
assert.strictEqual(entries.length, 1)
|
||||
assert.strictEqual(entries[0].event, "UserCreated")
|
||||
assert.strictEqual(entries[0].createdAtMillis, createdAt)
|
||||
}))
|
||||
|
||||
it.effect("writes remote entries and sequences", () =>
|
||||
Effect.gen(function*() {
|
||||
const journal = yield* makeJournal
|
||||
const remoteId = EventJournal.makeRemoteIdUnsafe()
|
||||
const initial = yield* journal.nextRemoteSequence(remoteId)
|
||||
assert.strictEqual(initial, 0)
|
||||
|
||||
const entryA = new EventJournal.Entry({
|
||||
id: EventJournal.makeEntryIdUnsafe(),
|
||||
event: "UserCreated",
|
||||
primaryKey: "user-2",
|
||||
payload: new Uint8Array([2])
|
||||
}, { disableChecks: true })
|
||||
const entryB = new EventJournal.Entry({
|
||||
id: EventJournal.makeEntryIdUnsafe(),
|
||||
event: "UserCreated",
|
||||
primaryKey: "user-3",
|
||||
payload: new Uint8Array([3])
|
||||
}, { disableChecks: true })
|
||||
const remoteEntries = [
|
||||
new EventJournal.RemoteEntry({ remoteSequence: 0, entry: entryA }),
|
||||
new EventJournal.RemoteEntry({ remoteSequence: 1, entry: entryB })
|
||||
]
|
||||
const seenConflicts: Array<ReadonlyArray<EventJournal.Entry>> = []
|
||||
yield* journal.writeFromRemote({
|
||||
remoteId,
|
||||
entries: remoteEntries,
|
||||
effect: ({ conflicts }) =>
|
||||
Effect.sync(() => {
|
||||
seenConflicts.push(conflicts)
|
||||
})
|
||||
})
|
||||
const next = yield* journal.nextRemoteSequence(remoteId)
|
||||
assert.strictEqual(next, 2)
|
||||
|
||||
yield* journal.write({
|
||||
event: "LocalCreated",
|
||||
primaryKey: "local-1",
|
||||
payload: new Uint8Array([4]),
|
||||
effect: () => Effect.void
|
||||
})
|
||||
const uncommitted = yield* journal.withRemoteUncommited(remoteId, (entries) => Effect.succeed(entries))
|
||||
assert.strictEqual(uncommitted.length, 1)
|
||||
assert.strictEqual(uncommitted[0].event, "LocalCreated")
|
||||
assert.strictEqual(seenConflicts.length, 2)
|
||||
assert.strictEqual(seenConflicts[0][0]?.idString, entryA.idString)
|
||||
assert.strictEqual(seenConflicts[1][0]?.idString, entryB.idString)
|
||||
}))
|
||||
})
|
||||
@@ -0,0 +1,249 @@
|
||||
import { SqliteClient } from "@effect/sql-sqlite-node"
|
||||
import { assert, describe, it } from "@effect/vitest"
|
||||
import { Effect, Layer, Queue, Redacted, Stream } from "effect"
|
||||
import * as EventJournal from "effect/unstable/eventlog/EventJournal"
|
||||
import * as EventLog from "effect/unstable/eventlog/EventLog"
|
||||
import * as EventLogEncryption from "effect/unstable/eventlog/EventLogEncryption"
|
||||
import * as EventLogMessage from "effect/unstable/eventlog/EventLogMessage"
|
||||
import type { StoreId } from "effect/unstable/eventlog/EventLogMessage"
|
||||
import * as EventLogServer from "effect/unstable/eventlog/EventLogServerEncrypted"
|
||||
import * as EventLogSessionAuth from "effect/unstable/eventlog/EventLogSessionAuth"
|
||||
import { makeGetIdentityRootSecretMaterial } from "effect/unstable/eventlog/internal/identityRootSecretDerivation"
|
||||
import * as SqlEventLogServer from "effect/unstable/eventlog/SqlEventLogServerEncrypted"
|
||||
import { Reactivity } from "effect/unstable/reactivity"
|
||||
import * as RpcTest from "effect/unstable/rpc/RpcTest"
|
||||
import * as SqlClient from "effect/unstable/sql/SqlClient"
|
||||
|
||||
const storeIdA = "store-a" as StoreId
|
||||
const storeIdB = "store-b" as StoreId
|
||||
|
||||
const makeEntry = (value: number) =>
|
||||
new EventJournal.Entry({
|
||||
id: EventJournal.makeEntryIdUnsafe(),
|
||||
event: "UserCreated",
|
||||
primaryKey: `user-${value}`,
|
||||
payload: new Uint8Array([value])
|
||||
}, { disableChecks: true })
|
||||
|
||||
const persistEntries = (
|
||||
encryption: EventLogEncryption.EventLogEncryption["Service"],
|
||||
identity: EventLog.Identity["Service"],
|
||||
entries: ReadonlyArray<EventJournal.Entry>
|
||||
) =>
|
||||
Effect.gen(function*() {
|
||||
const encrypted = yield* encryption.encrypt(identity, entries)
|
||||
return encrypted.encryptedEntries.map((encryptedEntry, index) =>
|
||||
new EventLogServer.PersistedEntry({
|
||||
entryId: entries[index].id,
|
||||
iv: encrypted.iv,
|
||||
encryptedEntry
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
const makePersistedEntry = (index: number, entryId = EventJournal.makeEntryIdUnsafe()) =>
|
||||
new EventLogServer.PersistedEntry({
|
||||
entryId,
|
||||
iv: new Uint8Array(12),
|
||||
encryptedEntry: Uint8Array.of(index)
|
||||
})
|
||||
|
||||
const getIdentityRootSecretMaterial = makeGetIdentityRootSecretMaterial(globalThis.crypto)
|
||||
|
||||
const makeAuthenticateRequest = Effect.fnUntraced(function*(options: {
|
||||
readonly identity: EventLog.Identity["Service"]
|
||||
readonly challenge: Uint8Array
|
||||
readonly remoteId: EventJournal.RemoteId
|
||||
}) {
|
||||
const rootSecretMaterial = yield* getIdentityRootSecretMaterial(options.identity)
|
||||
const signature = yield* EventLogSessionAuth.signSessionAuthPayload({
|
||||
remoteId: options.remoteId,
|
||||
challenge: options.challenge,
|
||||
publicKey: options.identity.publicKey,
|
||||
signingPublicKey: rootSecretMaterial.signingPublicKey,
|
||||
signingPrivateKey: Redacted.value(rootSecretMaterial.signingPrivateKey)
|
||||
})
|
||||
return new EventLogMessage.Authenticate({
|
||||
publicKey: options.identity.publicKey,
|
||||
signingPublicKey: rootSecretMaterial.signingPublicKey,
|
||||
signature,
|
||||
algorithm: "Ed25519"
|
||||
})
|
||||
})
|
||||
|
||||
describe("SqlEventLogServer", () => {
|
||||
it.effect("persists remote id across storage instances", () =>
|
||||
Effect.gen(function*() {
|
||||
const sql = yield* SqliteClient.make({ filename: ":memory:" })
|
||||
const storageA = yield* SqlEventLogServer.makeStorage().pipe(
|
||||
Effect.provideService(SqlClient.SqlClient, sql)
|
||||
)
|
||||
const storageB = yield* SqlEventLogServer.makeStorage().pipe(
|
||||
Effect.provideService(SqlClient.SqlClient, sql)
|
||||
)
|
||||
const idA = yield* storageA.getId
|
||||
const idB = yield* storageB.getId
|
||||
assert.deepStrictEqual(idA, idB)
|
||||
}).pipe(Effect.provide([Reactivity.layer, EventLogEncryption.layerSubtle])))
|
||||
|
||||
it.effect("rejects session-auth rebinding for an existing publicKey", () =>
|
||||
Effect.gen(function*() {
|
||||
const sql = yield* SqliteClient.make({ filename: ":memory:" })
|
||||
const storage = yield* SqlEventLogServer.makeStorage().pipe(
|
||||
Effect.provideService(SqlClient.SqlClient, sql)
|
||||
)
|
||||
const rpcClient = yield* RpcTest.makeClient(EventLogMessage.EventLogRemoteRpcs).pipe(
|
||||
Effect.provide(
|
||||
EventLogServer.layerRpcHandlers.pipe(
|
||||
Layer.provide(Layer.succeed(EventLogServer.Storage, storage))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
const firstIdentity = yield* EventLog.makeIdentity
|
||||
const secondIdentitySeed = yield* EventLog.makeIdentity
|
||||
const secondIdentity: EventLog.Identity["Service"] = {
|
||||
publicKey: firstIdentity.publicKey,
|
||||
privateKey: secondIdentitySeed.privateKey
|
||||
}
|
||||
|
||||
const firstMaterial = yield* getIdentityRootSecretMaterial(firstIdentity)
|
||||
const secondMaterial = yield* getIdentityRootSecretMaterial(secondIdentity)
|
||||
const sameSigningPublicKey =
|
||||
firstMaterial.signingPublicKey.byteLength === secondMaterial.signingPublicKey.byteLength &&
|
||||
firstMaterial.signingPublicKey.every((byte, index) => byte === secondMaterial.signingPublicKey[index])
|
||||
assert.strictEqual(sameSigningPublicKey, false)
|
||||
|
||||
const firstHello = yield* rpcClient["EventLog.Hello"]()
|
||||
yield* rpcClient["EventLog.Authenticate"](
|
||||
yield* makeAuthenticateRequest({
|
||||
identity: firstIdentity,
|
||||
challenge: firstHello.challenge,
|
||||
remoteId: firstHello.remoteId
|
||||
})
|
||||
)
|
||||
|
||||
const secondHello = yield* rpcClient["EventLog.Hello"]()
|
||||
const error = yield* rpcClient["EventLog.Authenticate"](
|
||||
yield* makeAuthenticateRequest({
|
||||
identity: secondIdentity,
|
||||
challenge: secondHello.challenge,
|
||||
remoteId: secondHello.remoteId
|
||||
})
|
||||
).pipe(Effect.flip)
|
||||
|
||||
assert.instanceOf(error, EventLogMessage.EventLogProtocolError)
|
||||
assert.strictEqual(error.code, "Forbidden")
|
||||
assert.strictEqual(error.message, "Session auth signature verification failed")
|
||||
}).pipe(Effect.provide([Reactivity.layer, EventLogEncryption.layerSubtle])))
|
||||
|
||||
it.effect("writes entries and streams changes", () =>
|
||||
Effect.gen(function*() {
|
||||
const sql = yield* SqliteClient.make({ filename: ":memory:" })
|
||||
const storage = yield* SqlEventLogServer.makeStorage().pipe(
|
||||
Effect.provideService(SqlClient.SqlClient, sql)
|
||||
)
|
||||
const encryption = yield* EventLogEncryption.EventLogEncryption
|
||||
const identity = yield* encryption.generateIdentity
|
||||
const entries = [makeEntry(1), makeEntry(2)]
|
||||
const persisted = yield* persistEntries(encryption, identity, entries)
|
||||
const written = yield* storage.write(identity.publicKey, storeIdA, persisted)
|
||||
assert.deepStrictEqual(written.map((entry) => entry.sequence), [1, 2])
|
||||
|
||||
const changes = yield* storage.changes(identity.publicKey, storeIdA, 0).pipe(
|
||||
Stream.toQueue({ capacity: "unbounded" })
|
||||
)
|
||||
const taken = yield* Queue.takeAll(changes)
|
||||
assert.deepStrictEqual(taken.map((entry) => entry.sequence), [1, 2])
|
||||
|
||||
const nextEntry = makeEntry(3)
|
||||
const nextPersisted = yield* persistEntries(encryption, identity, [nextEntry])
|
||||
const updated = yield* storage.write(identity.publicKey, storeIdA, nextPersisted)
|
||||
assert.deepStrictEqual(updated.map((entry) => entry.sequence), [3])
|
||||
|
||||
const next = yield* Queue.take(changes)
|
||||
assert.strictEqual(next.sequence, 3)
|
||||
}).pipe(Effect.provide([Reactivity.layer, EventLogEncryption.layerSubtle])))
|
||||
|
||||
it.effect("isolates same publicKey across storeIds", () =>
|
||||
Effect.gen(function*() {
|
||||
const sql = yield* SqliteClient.make({ filename: ":memory:" })
|
||||
const storage = yield* SqlEventLogServer.makeStorage().pipe(
|
||||
Effect.provideService(SqlClient.SqlClient, sql)
|
||||
)
|
||||
|
||||
yield* storage.write("client-1", storeIdA, [makePersistedEntry(1)])
|
||||
yield* storage.write("client-1", storeIdB, [makePersistedEntry(2)])
|
||||
|
||||
const storeAEntries = yield* storage.changes("client-1", storeIdA, 0).pipe(
|
||||
Stream.take(1),
|
||||
Stream.runCollect
|
||||
)
|
||||
const storeBEntries = yield* storage.changes("client-1", storeIdB, 0).pipe(
|
||||
Stream.take(1),
|
||||
Stream.runCollect
|
||||
)
|
||||
|
||||
assert.deepStrictEqual(storeAEntries.map((entry) => entry.sequence), [1])
|
||||
assert.deepStrictEqual(storeBEntries.map((entry) => entry.sequence), [1])
|
||||
}).pipe(Effect.provide([Reactivity.layer, EventLogEncryption.layerSubtle])))
|
||||
|
||||
it.effect("isolates same storeId across publicKeys", () =>
|
||||
Effect.gen(function*() {
|
||||
const sql = yield* SqliteClient.make({ filename: ":memory:" })
|
||||
const storage = yield* SqlEventLogServer.makeStorage().pipe(
|
||||
Effect.provideService(SqlClient.SqlClient, sql)
|
||||
)
|
||||
|
||||
yield* storage.write("client-1", storeIdA, [makePersistedEntry(1)])
|
||||
yield* storage.write("client-2", storeIdA, [makePersistedEntry(2)])
|
||||
|
||||
const clientOneEntries = yield* storage.changes("client-1", storeIdA, 0).pipe(
|
||||
Stream.take(1),
|
||||
Stream.runCollect
|
||||
)
|
||||
const clientTwoEntries = yield* storage.changes("client-2", storeIdA, 0).pipe(
|
||||
Stream.take(1),
|
||||
Stream.runCollect
|
||||
)
|
||||
|
||||
assert.deepStrictEqual(clientOneEntries.map((entry) => entry.sequence), [1])
|
||||
assert.deepStrictEqual(clientTwoEntries.map((entry) => entry.sequence), [1])
|
||||
}).pipe(Effect.provide([Reactivity.layer, EventLogEncryption.layerSubtle])))
|
||||
|
||||
it.effect("keeps deduplication isolated per encrypted scope", () =>
|
||||
Effect.gen(function*() {
|
||||
const sql = yield* SqliteClient.make({ filename: ":memory:" })
|
||||
const storage = yield* SqlEventLogServer.makeStorage().pipe(
|
||||
Effect.provideService(SqlClient.SqlClient, sql)
|
||||
)
|
||||
const sharedEntryId = EventJournal.makeEntryIdUnsafe()
|
||||
|
||||
yield* storage.write("client-1", storeIdA, [makePersistedEntry(1, sharedEntryId)])
|
||||
yield* storage.write("client-1", storeIdA, [makePersistedEntry(2, sharedEntryId)])
|
||||
yield* storage.write("client-1", storeIdB, [makePersistedEntry(3, sharedEntryId)])
|
||||
yield* storage.write("client-2", storeIdA, [makePersistedEntry(4, sharedEntryId)])
|
||||
|
||||
assert.deepStrictEqual(
|
||||
(yield* storage.changes("client-1", storeIdA, 0).pipe(
|
||||
Stream.take(1),
|
||||
Stream.runCollect
|
||||
)).map((entry) => entry.sequence),
|
||||
[1]
|
||||
)
|
||||
assert.deepStrictEqual(
|
||||
(yield* storage.changes("client-1", storeIdB, 0).pipe(
|
||||
Stream.take(1),
|
||||
Stream.runCollect
|
||||
)).map((entry) => entry.sequence),
|
||||
[1]
|
||||
)
|
||||
assert.deepStrictEqual(
|
||||
(yield* storage.changes("client-2", storeIdA, 0).pipe(
|
||||
Stream.take(1),
|
||||
Stream.runCollect
|
||||
)).map((entry) => entry.sequence),
|
||||
[1]
|
||||
)
|
||||
}).pipe(Effect.provide([Reactivity.layer, EventLogEncryption.layerSubtle])))
|
||||
})
|
||||
@@ -0,0 +1,110 @@
|
||||
import { SqliteClient } from "@effect/sql-sqlite-node"
|
||||
import { assert, describe, it } from "@effect/vitest"
|
||||
import { Effect, Layer, Redacted } from "effect"
|
||||
import * as SqlEventLogServerUnencryptedStorageTest from "effect-test/unstable/eventlog/SqlEventLogServerUnencryptedStorageTest"
|
||||
import type * as EventJournal from "effect/unstable/eventlog/EventJournal"
|
||||
import * as EventLog from "effect/unstable/eventlog/EventLog"
|
||||
import * as EventLogEncryption from "effect/unstable/eventlog/EventLogEncryption"
|
||||
import * as EventLogMessage from "effect/unstable/eventlog/EventLogMessage"
|
||||
import * as EventLogServerUnencrypted from "effect/unstable/eventlog/EventLogServerUnencrypted"
|
||||
import * as EventLogSessionAuth from "effect/unstable/eventlog/EventLogSessionAuth"
|
||||
import { makeGetIdentityRootSecretMaterial } from "effect/unstable/eventlog/internal/identityRootSecretDerivation"
|
||||
import * as SqlEventLogServerUnencrypted from "effect/unstable/eventlog/SqlEventLogServerUnencrypted"
|
||||
import { Reactivity } from "effect/unstable/reactivity"
|
||||
import * as RpcTest from "effect/unstable/rpc/RpcTest"
|
||||
import * as SqlClient from "effect/unstable/sql/SqlClient"
|
||||
|
||||
SqlEventLogServerUnencryptedStorageTest.suite(
|
||||
"sql-sqlite-node",
|
||||
SqliteClient.layer({
|
||||
filename: ":memory:"
|
||||
})
|
||||
)
|
||||
|
||||
const getIdentityRootSecretMaterial = makeGetIdentityRootSecretMaterial(globalThis.crypto)
|
||||
|
||||
const makeAuthenticateRequest = Effect.fnUntraced(function*(options: {
|
||||
readonly identity: EventLog.Identity["Service"]
|
||||
readonly challenge: Uint8Array
|
||||
readonly remoteId: EventJournal.RemoteId
|
||||
}) {
|
||||
const rootSecretMaterial = yield* getIdentityRootSecretMaterial(options.identity)
|
||||
const signature = yield* EventLogSessionAuth.signSessionAuthPayload({
|
||||
remoteId: options.remoteId,
|
||||
challenge: options.challenge,
|
||||
publicKey: options.identity.publicKey,
|
||||
signingPublicKey: rootSecretMaterial.signingPublicKey,
|
||||
signingPrivateKey: Redacted.value(rootSecretMaterial.signingPrivateKey)
|
||||
})
|
||||
return new EventLogMessage.Authenticate({
|
||||
publicKey: options.identity.publicKey,
|
||||
signingPublicKey: rootSecretMaterial.signingPublicKey,
|
||||
signature,
|
||||
algorithm: "Ed25519"
|
||||
})
|
||||
})
|
||||
|
||||
describe("SqlEventLogServerUnencrypted (sql-sqlite-node)", () => {
|
||||
it.effect("rejects session-auth rebinding for an existing publicKey", () =>
|
||||
Effect.gen(function*() {
|
||||
const sql = yield* SqliteClient.make({ filename: ":memory:" })
|
||||
const storage = yield* SqlEventLogServerUnencrypted.makeStorage().pipe(
|
||||
Effect.provideService(SqlClient.SqlClient, sql),
|
||||
Effect.orDie
|
||||
)
|
||||
|
||||
const rpcClient = yield* RpcTest.makeClient(EventLogMessage.EventLogRemoteRpcs).pipe(
|
||||
Effect.provide(
|
||||
EventLogServerUnencrypted.layerRpcHandlers.pipe(
|
||||
Layer.provideMerge(EventLog.layerRegistry),
|
||||
Layer.provide(Layer.succeed(EventLogServerUnencrypted.Storage, storage)),
|
||||
Layer.provide(Layer.succeed(EventLogServerUnencrypted.StoreMapping, {
|
||||
resolve: ({ storeId }) => Effect.succeed(storeId),
|
||||
hasStore: () => Effect.succeed(true)
|
||||
})),
|
||||
Layer.provide(Layer.succeed(EventLogServerUnencrypted.EventLogServerAuthorization, {
|
||||
authorizeWrite: () => Effect.void,
|
||||
authorizeRead: () => Effect.void,
|
||||
authorizeIdentity: () => Effect.void
|
||||
}))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
const firstIdentity = yield* EventLog.makeIdentity
|
||||
const secondIdentitySeed = yield* EventLog.makeIdentity
|
||||
const secondIdentity: EventLog.Identity["Service"] = {
|
||||
publicKey: firstIdentity.publicKey,
|
||||
privateKey: secondIdentitySeed.privateKey
|
||||
}
|
||||
|
||||
const firstMaterial = yield* getIdentityRootSecretMaterial(firstIdentity)
|
||||
const secondMaterial = yield* getIdentityRootSecretMaterial(secondIdentity)
|
||||
const sameSigningPublicKey =
|
||||
firstMaterial.signingPublicKey.byteLength === secondMaterial.signingPublicKey.byteLength &&
|
||||
firstMaterial.signingPublicKey.every((byte, index) => byte === secondMaterial.signingPublicKey[index])
|
||||
assert.strictEqual(sameSigningPublicKey, false)
|
||||
|
||||
const firstHello = yield* rpcClient["EventLog.Hello"]()
|
||||
yield* rpcClient["EventLog.Authenticate"](
|
||||
yield* makeAuthenticateRequest({
|
||||
identity: firstIdentity,
|
||||
challenge: firstHello.challenge,
|
||||
remoteId: firstHello.remoteId
|
||||
})
|
||||
)
|
||||
|
||||
const secondHello = yield* rpcClient["EventLog.Hello"]()
|
||||
const error = yield* rpcClient["EventLog.Authenticate"](
|
||||
yield* makeAuthenticateRequest({
|
||||
identity: secondIdentity,
|
||||
challenge: secondHello.challenge,
|
||||
remoteId: secondHello.remoteId
|
||||
})
|
||||
).pipe(Effect.flip)
|
||||
|
||||
assert.instanceOf(error, EventLogMessage.EventLogProtocolError)
|
||||
assert.strictEqual(error.code, "Forbidden")
|
||||
assert.strictEqual(error.message, "Session auth signature verification failed")
|
||||
}).pipe(Effect.provide([Reactivity.layer, EventLogEncryption.layerSubtle])))
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { SqliteClient, SqliteMigrator } from "@effect/sql-sqlite-node"
|
||||
import { assert, describe, it } from "@effect/vitest"
|
||||
import { Effect, FileSystem } from "effect"
|
||||
import { Reactivity } from "effect/unstable/reactivity"
|
||||
import * as SqlClient from "effect/unstable/sql/SqlClient"
|
||||
import * as SqlError from "effect/unstable/sql/SqlError"
|
||||
|
||||
const makeClients = Effect.gen(function*() {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const dir = yield* fs.makeTempDirectoryScoped()
|
||||
const filename = dir + "/test.db"
|
||||
|
||||
return {
|
||||
lockClient: yield* SqliteClient.make({ filename }),
|
||||
migratorClient: yield* SqliteClient.make({ filename })
|
||||
}
|
||||
}).pipe(Effect.provide([NodeFileSystem.layer, Reactivity.layer]))
|
||||
|
||||
describe("SqliteMigrator", () => {
|
||||
it.effect("fails on lock errors", () =>
|
||||
Effect.gen(function*() {
|
||||
const { lockClient, migratorClient } = yield* makeClients
|
||||
|
||||
yield* migratorClient`PRAGMA busy_timeout = 1`
|
||||
|
||||
yield* SqliteMigrator.run({
|
||||
loader: SqliteMigrator.fromRecord({})
|
||||
}).pipe(Effect.provideService(SqlClient.SqlClient, migratorClient))
|
||||
|
||||
yield* Effect.acquireRelease(
|
||||
lockClient`BEGIN IMMEDIATE`,
|
||||
() => lockClient`ROLLBACK`.pipe(Effect.ignore)
|
||||
)
|
||||
|
||||
const error = yield* Effect.flip(
|
||||
SqliteMigrator.run({
|
||||
loader: SqliteMigrator.fromRecord({
|
||||
"1_test": Effect.void
|
||||
})
|
||||
}).pipe(Effect.provideService(SqlClient.SqlClient, migratorClient))
|
||||
)
|
||||
|
||||
assert.strictEqual(error._tag, "SqlError")
|
||||
assert(SqlError.isSqlError(error))
|
||||
assert.strictEqual(error.reason._tag, "LockTimeoutError")
|
||||
}))
|
||||
})
|
||||
11
repos/effect-smol/packages/sql/sqlite-node/tsconfig.json
Normal file
11
repos/effect-smol/packages/sql/sqlite-node/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/tsconfig",
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{ "path": "../../effect" }
|
||||
],
|
||||
"compilerOptions": {
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { mergeConfig, type ViteUserConfig } from "vitest/config"
|
||||
import shared from "../../../vitest.shared.ts"
|
||||
|
||||
const config: ViteUserConfig = {}
|
||||
|
||||
export default mergeConfig(shared, config)
|
||||
Reference in New Issue
Block a user