From b69c24c1b2ef22295fcb474bb4351599047f1406 Mon Sep 17 00:00:00 2001 From: Marshall Thompson Date: Tue, 2 Apr 2024 22:53:59 -0600 Subject: [PATCH] feat: kysely adapter initial commit --- README.md | 2 +- package-lock.json | 34 ++- packages/kysely/LICENSE | 22 ++ packages/kysely/README.md | 23 ++ packages/kysely/package.json | 64 +++++ packages/kysely/src/error-handler.ts | 96 +++++++ packages/kysely/src/index.ts | 389 +++++++++++++++++++++++++++ packages/kysely/test/index.test.ts | 99 +++++++ packages/kysely/tsconfig.json | 9 + 9 files changed, 734 insertions(+), 4 deletions(-) create mode 100644 packages/kysely/LICENSE create mode 100644 packages/kysely/README.md create mode 100644 packages/kysely/package.json create mode 100644 packages/kysely/src/error-handler.ts create mode 100644 packages/kysely/src/index.ts create mode 100644 packages/kysely/test/index.test.ts create mode 100644 packages/kysely/tsconfig.json diff --git a/README.md b/README.md index a281d98..a2efeae 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,6 @@ npm run generate:adapter # License -Copyright (c) 2023 [Feathers contributors](https://github.com/feathersjs/feathers/graphs/contributors) +Copyright (c) 2024 [Feathers contributors](https://github.com/feathersjs/feathers/graphs/contributors) Licensed under the [MIT license](LICENSE). diff --git a/package-lock.json b/package-lock.json index e7a834a..4409744 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1044,9 +1044,9 @@ } }, "node_modules/@feathersjs/errors": { - "version": "5.0.11", - "resolved": "https://registry.npmjs.org/@feathersjs/errors/-/errors-5.0.11.tgz", - "integrity": "sha512-rFVFgGOoQmOh+B8n/Xef3fLDaJaewD3TeVo451lBZjjqQZqgS4XrsEVOKjFtj0ozRBfiZ7AzkzlZEgqDO9syvw==", + "version": "5.0.24", + "resolved": "https://registry.npmjs.org/@feathersjs/errors/-/errors-5.0.24.tgz", + "integrity": "sha512-7XnTWogwA1da3VT3zjjD+oaMZXtxueadm5gP9e4+aD8dYcW6djPk2KvBL38OWEhXZ/iMgPpA7p0JETcbDYE44w==", "engines": { "node": ">= 12" } @@ -3226,6 +3226,10 @@ "resolved": "packages/knex", "link": true }, + "node_modules/@wingshq/kysely": { + "resolved": "packages/kysely", + "link": true + }, "node_modules/@wingshq/memory": { "resolved": "packages/memory", "link": true @@ -7481,6 +7485,14 @@ "node": ">=8" } }, + "node_modules/kysely": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.27.3.tgz", + "integrity": "sha512-lG03Ru+XyOJFsjH3OMY6R/9U38IjDPfnOfDgO3ynhbDr+Dz8fak+X6L62vqu3iybQnj+lG84OttBuU9KY3L9kA==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/latest-version": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", @@ -13436,6 +13448,22 @@ "node": ">= 20" } }, + "packages/kysely": { + "name": "@wingshq/kysely", + "version": "0.0.0", + "license": "MIT", + "dependencies": { + "@feathersjs/errors": "^5.0.24", + "@wingshq/adapter-commons": "^0.0.0", + "kysely": "^0.27.3" + }, + "devDependencies": { + "@wingshq/adapter-tests": "^0.0.0" + }, + "engines": { + "node": ">= 20" + } + }, "packages/memory": { "name": "@wingshq/memory", "version": "0.0.0", diff --git a/packages/kysely/LICENSE b/packages/kysely/LICENSE new file mode 100644 index 0000000..44afb12 --- /dev/null +++ b/packages/kysely/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2024 Wings 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. + \ No newline at end of file diff --git a/packages/kysely/README.md b/packages/kysely/README.md new file mode 100644 index 0000000..8b1f660 --- /dev/null +++ b/packages/kysely/README.md @@ -0,0 +1,23 @@ +# @wingshq/kysely + +[![CI](https://github.com/wingshq/kysely/workflows/CI/badge.svg)](https://github.com/wingshq/wings/actions?query=workflow%3ACI) +[![Download Status](https://img.shields.io/npm/dm/@wingshq/kysely.svg?style=flat-square)](https://www.npmjs.com/package/@wingshq/kysely) +[![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/qa8kez8QBx) + +> A Wings adapter for Kysely + +## Installation + +```bash +$ npm install --save @wingshq/kysely +``` + +## Documentation + +See [Wings kysely Adapter API documentation](https://wings.codes/adapters/kysely.html) for more details. + +## License + +Copyright (c) 2024 [Wings contributors](https://github.com/wingshq/wings/graphs/contributors) + +Licensed under the [MIT license](LICENSE). diff --git a/packages/kysely/package.json b/packages/kysely/package.json new file mode 100644 index 0000000..f8aeebf --- /dev/null +++ b/packages/kysely/package.json @@ -0,0 +1,64 @@ +{ + "name": "@wingshq/kysely", + "description": "A Wings adapter for Kysely", + "version": "0.0.0", + "homepage": "https://wings.codes", + "keywords": [ + "wings", + "wings-adapter" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git://github.com/wingshq/wings.git", + "directory": "packages/kysely" + }, + "author": { + "name": "Wings contributors", + "email": "hello@feathersjs.com", + "url": "https://feathersjs.com" + }, + "contributors": [], + "bugs": { + "url": "https://github.com/wingshq/wings/issues" + }, + "engines": { + "node": ">= 20" + }, + "files": [ + "CHANGELOG.md", + "LICENSE", + "README.md", + "src/**", + "lib/**", + "esm/**" + ], + "module": "./esm/index.js", + "main": "./lib/index.js", + "types": "./src/index.ts", + "exports": { + ".": { + "import": "./esm/index.js", + "require": "./lib/index.js", + "types": "./src/index.ts" + } + }, + "scripts": { + "prepublish": "npm run compile", + "compile:lib": "shx rm -rf lib/ && tsc --module commonjs", + "compile:esm": "shx rm -rf esm/ && tsc --module es2020 --outDir esm", + "compile": "npm run compile:lib && npm run compile:esm", + "test": "npm run compile && node --require ts-node/register --test test/**.test.ts" + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@feathersjs/errors": "^5.0.24", + "@wingshq/adapter-commons": "^0.0.0", + "kysely": "^0.27.3" + }, + "devDependencies": { + "@wingshq/adapter-tests": "^0.0.0" + } +} diff --git a/packages/kysely/src/error-handler.ts b/packages/kysely/src/error-handler.ts new file mode 100644 index 0000000..5a0fa54 --- /dev/null +++ b/packages/kysely/src/error-handler.ts @@ -0,0 +1,96 @@ +import { errors } from '@feathersjs/errors' + +export const ERROR = Symbol.for('feathers-kysely/error') + +/** + * Returns the correct Feathers Error depending on the SQL error. + * @param error + * @returns a type of FeathersError + */ +export function errorHandler(error: any, params: any) { + const { message } = error + console.error(message) + + let feathersError = error + + if (error.sqlState && error.sqlState.length) { + // remove SQLSTATE marker (#) and pad/truncate SQLSTATE to 5 chars + const sqlState = `00000${error.sqlState.replace('#', '')}`.slice(-5) + + switch (sqlState.slice(0, 2)) { + case '02': + feathersError = new errors.NotFound(message) + break + case '28': + feathersError = new errors.Forbidden(message) + break + case '08': + case '0A': + case '0K': + feathersError = new errors.Unavailable(message) + break + case '20': + case '21': + case '22': + case '23': + case '24': + case '25': + case '40': + case '42': + case '70': + feathersError = new errors.BadRequest(message) + break + default: + feathersError = new errors.GeneralError(message) + } + } else if (error.message.includes('SqliteError') || error.message.includes('D1_TYPE_ERROR')) { + let message = error.message + + // remove SqliteError marker + message = message.replace('SqliteError: ', '') + + const errorData = { query: JSON.stringify(params.query) } + + // remove D1_ERROR marker + if (message.includes('D1_ERROR: ')) message = message.replace('D1_ERROR: ', '') + + if (message.includes('UNIQUE constraint failed:')) + feathersError = new errors.BadRequest(message, errorData) + + if (message.includes('not supported for value')) feathersError = new errors.BadRequest(message, errorData) + else feathersError = new errors.GeneralError(message, errorData) + } else if (typeof error.code === 'string' && error.severity && error.routine) { + // NOTE: Error codes taken from + // https://www.postgresql.org/docs/9.6/static/errcodes-appendix.html + // Omit query information + const messages = (error.message || '').split('-') + + error.message = messages[messages.length - 1] + + switch (error.code.slice(0, 2)) { + case '22': + feathersError = new errors.NotFound(message) + break + case '23': + feathersError = new errors.BadRequest(message) + break + case '28': + feathersError = new errors.Forbidden(message) + break + case '3D': + case '3F': + case '42': + feathersError = new errors.Unprocessable(message) + break + default: + feathersError = new errors.GeneralError(message) + break + } + } else if (!(error instanceof errors.FeathersError)) { + feathersError = new errors.GeneralError(message) + } + + // feathersError[ERROR] = error + + return feathersError +} diff --git a/packages/kysely/src/index.ts b/packages/kysely/src/index.ts new file mode 100644 index 0000000..188fac0 --- /dev/null +++ b/packages/kysely/src/index.ts @@ -0,0 +1,389 @@ +import { + AdapterInterface, + AdapterOptions, + AdapterParams, + AdapterQuery, + Id, + Paginated, + QueryProperty +} from '@wingshq/adapter-commons' +import { _ } from '@feathersjs/commons' +import { BadRequest, NotFound } from '@feathersjs/errors' + +import type { + ComparisonOperatorExpression, + DeleteQueryBuilder, + DeleteResult, + InsertQueryBuilder, + InsertResult, + Kysely, + SelectQueryBuilder, + UpdateQueryBuilder, + UpdateResult, + Transaction, + TableExpression +} from 'kysely' +import { errorHandler } from './error-handler' + +// See https://kysely-org.github.io/kysely/variables/OPERATORS.html +const OPERATORS: Record = { + $lt: '<', + $lte: '<=', + $gt: '>', + $gte: '>=', + $in: 'in', + $nin: 'not in', + $like: 'like', + $notlike: 'not like', + $ilike: 'ilike', + $ne: '!=', + $is: 'is', + $isnot: 'is not' +} + +type DeleteOrInsertBuilder = + | DeleteQueryBuilder + | InsertQueryBuilder + | UpdateQueryBuilder + +export interface KyselyOptions extends AdapterOptions { + Model: Kysely + /** + * The table name + */ + name: TableExpression +} + +export type KyselyQueryProperty = { + $like?: `%${string}%` | `%${string}` | `${string}%` +} & QueryProperty + +export type KyselyQueryProperties = { + [k in keyof T]?: T[k] | KyselyQueryProperty +} + +export type KyselyQuery = KyselyQueryProperties & + Partial, '$limit' | '$skip' | '$sort' | '$select'>> & { + $or?: KyselyQueryProperties[] + $and?: KyselyQueryProperties[] + } + +export interface KyselyParams extends AdapterParams> { + Model?: Kysely + transaction?: Transaction +} + +export class KyselyAdapter< + Tables = unknown, + Result = unknown, + Data = Partial, + PatchData = Partial, + UpdateData = Data, + Params extends KyselyParams = KyselyParams +> implements AdapterInterface, Params> +{ + constructor(public options: KyselyOptions) { + if (!options || !options.Model) { + throw new Error('You must provide a Model (the Kysely db object)') + } + + if (typeof options.name !== 'string') { + throw new Error('No table name specified.') + } + + this.options = { + id: 'id', + ...options + } + } + + get id() { + return this.options.id + } + + getQuery(params?: Params) { + const { $skip, $sort, $limit, $select, ...query } = params?.query || {} + + return { + query: query as KyselyQuery, + filters: { $skip, $sort, $limit, $select } + } + } + + createQuery(options: KyselyOptions, filters: any, query: any) { + const q = this.startSelectQuery(options, filters) + const qWhere = this.applyWhere(q, query) + const qLimit = filters.$limit ? qWhere.limit(filters.$limit) : qWhere + const qSkip = filters.$skip ? qLimit.offset(filters.$skip) : qLimit + const qSorted = this.applySort(qSkip as any, filters) + return qSorted + } + + startSelectQuery(options: KyselyOptions, filters: any) { + const { name, id: idField, Model } = options + const q = Model.selectFrom(name) + return filters.$select ? q.select(filters.$select.concat(idField)) : q.selectAll() + } + + createCountQuery(params: Params) { + const { query } = this.getQuery(params) // Extract only the query part from the parameters + const { Model = this.options.Model } = params || {} + + // Start a new select query + const q = Model.selectFrom(this.options.name as any) + + // Apply the WHERE conditions based on the query parameters + const qWhere = this.applyWhere(q, query) + + // Select only the count of 'id', not all columns + const countParams = Model.fn.count(this.id as any).as('total') + return qWhere.select(countParams) + } + + applyWhere>(q: Q, query: KyselyQuery) { + // loop through params and call the where filters + return Object.entries(query).reduce((q, [key, value]: any) => { + // if (key === '$or') { + // return value.reduce((q: Q, subParams: Query) => { + // return q.orWhere((subQ: Q) => this.applyWhere(subQ, subParams)) + // }, q) + // } else + if (['$and', '$or'].includes(key)) { + return q.where((qb: any) => { + return this.handleAndOr(qb, key, value) + }) + } else if (_.isObject(value)) { + // loop through OPERATORS and apply them + const qOperators = Object.entries(OPERATORS).reduce((q, [operator, op]) => { + if (value && Object.prototype.hasOwnProperty.call(value, operator)) { + const val = value[operator] + if (val === null) { + const nullOperator = operator === '$ne' ? 'is not' : 'is' + return q.where(key, nullOperator, val) + } else { + return q.where(key, op, value[operator]) + } + } + return q + }, q) + return qOperators + } else { + if (value === null) return q.where(key, 'is', value) + else return q.where(key, '=', value) + } + }, q) + } + + handleAndOr(qb: any, key: string, value: KyselyQueryProperties[]) { + const method = qb[key.replace('$', '')] + const subs = value.map((subParams: KyselyQuery) => { + return this.handleSubQuery(qb, subParams) + }) + return method(subs) + } + + handleSubQuery(qb: any, query: KyselyQuery): any { + return qb.and( + Object.entries(query).map(([key, value]: any) => { + if (['$and', '$or'].includes(key)) { + return this.handleAndOr(qb, key, value) + } else if (_.isObject(value)) { + // loop through OPERATORS and apply them + return qb.and( + Object.entries(OPERATORS) + .filter(([operator, _op]) => { + return value && Object.prototype.hasOwnProperty.call(value, operator) + }) + .map(([operator, op]) => { + const val = value[operator] + return this.whereCompare(qb, key, op, val) + }) + ) + } else { + return this.whereCompare(qb, key, '=', value) + } + }) + ) + } + + whereCompare(qb: any, key: string, operator: any, value: any) { + if (value === null) { + const nullOperator = operator === '$ne' ? 'is not' : 'is' + return qb.cmpr(key, nullOperator, value) + } else { + return qb.cmpr(key, operator, value) + } + } + + applySort>>(q: Q, filters: any) { + return Object.entries(filters.$sort || {}).reduce( + (q, [key, value]) => { + return q.orderBy(key, value === 1 ? 'asc' : 'desc') + }, + q as SelectQueryBuilder> + ) + } + + /** + * Add a returning statement alias for each key (bypasses bug in sqlite) + * @param q kysely query builder + * @param data data which is expected to be returned + */ + applyReturning(q: Q, keys: string[]) { + return keys.reduce((q: any, key) => { + return q.returning(`${key} as ${key}`) + }, q.returningAll()) + } + + async find(params: Params & { paginate: true }): Promise> + async find(params?: Params & { paginate?: false }): Promise + async find(params?: Params & { paginate?: boolean }): Promise> { + const { filters, query } = this.getQuery(params) + const q = this.createQuery(this.options, filters, query) + + try { + if (params?.paginate) { + const countQuery: any = this.createCountQuery(params) + const [queryResult, countQueryResult] = await Promise.all([q.execute(), countQuery.execute()]) + + const data = filters.$limit === 0 ? [] : queryResult + const total = Number.parseInt(countQueryResult[0].total) + + return { + total, + limit: filters.$limit, + skip: filters.$skip || 0, + data: data as Result[] + } + } + const data = filters.$limit === 0 ? [] : await q.execute() + return data as Result[] + } catch (error) { + throw errorHandler(error, params) + } + } + + async get(id: Id, params?: Params): Promise { + const { filters, query } = this.getQuery(params) as any + + if (!id && id !== null && !query[this.id] && query[this.id] !== null) + throw new NotFound(`No record found for id ${id}`) + + const q = this.startSelectQuery(this.options, filters) + const qWhere = this.applyWhere(q, { [this.id]: id, ...query }) + try { + const item = await qWhere.executeTakeFirst() + + if (!item) throw new NotFound(`No record found for ${this.id} '${id}'`) + + return item as Result + } catch (error) { + throw errorHandler(error, params) + } + } + + async create(data: Data[], params?: Params): Promise + async create(data: Data, params?: Params): Promise + async create(data: Data | Data[], params?: Params): Promise { + const idField: any = this.id + const { Model = this.options.Model } = params || {} + const { name } = this.options + const { filters } = this.getQuery(params) + const isArray = Array.isArray(data) + const $select: any = filters.$select?.length ? filters.$select.concat(idField) : [] + + const q = Model.insertInto(name as any).values(data as any) + + const qReturning = this.applyReturning(q, Object.keys(data as Record)) + + const request = isArray ? qReturning.execute() : qReturning.executeTakeFirst() + + try { + const response = await request + const toReturn = filters.$select?.length + ? isArray + ? (response as Result[]).map((i: any) => _.pick(i, ...$select)) + : _.pick(response, ...$select) + : response + + return toReturn as Result | Result[] + } catch (error) { + throw errorHandler(error, params) + } + } + + async update(id: Id, _data: UpdateData, params?: Params): Promise { + if (id === null || Array.isArray(_data)) + throw new BadRequest("You can not replace multiple instances. Did you mean 'patch'?") + + const data = _.omit(_data, this.id) + const oldData = await this.get(id, params) + // New data changes all fields except id + const newObject = Object.keys(oldData as any).reduce((result: any, key) => { + if (key !== this.id) result[key] = data[key] === undefined ? null : data[key] + + return result + }, {}) + + const result = await this.patch(id, newObject, params) + + return result as Result + } + + async patch(id: Id, data: PatchData, params?: Params): Promise + async patch(id: null, data: PatchData, params?: Params): Promise + async patch(id: Id | null, _data: PatchData, params?: Params): Promise { + const asMulti = id === null + const { name } = this.options + const { filters, query } = this.getQuery(params) as any + const $select = filters.$select?.length ? filters.$select.concat(this.id as any) : [] + const { Model = this.options.Model } = params || {} + + if (id != null && query[this.id] != null) throw new NotFound() + + const q = Model.updateTable(name as any).set(_.omit(_data, this.id)) + const qWhere = this.applyWhere(q, asMulti ? query : id == null ? query : { [this.id]: id, ...query }) + const toSelect = filters.$select?.length ? filters.$select : Object.keys(_data as any) + const qReturning = this.applyReturning(qWhere, toSelect.concat(this.id)) + + const request = asMulti ? qReturning.execute() : qReturning.executeTakeFirst() + try { + const response = await request + + if (!asMulti && !response) throw new NotFound(`No record found for ${this.id} '${id}'`) + + const toReturn = filters.$select?.length + ? Array.isArray(response) + ? response.map((i: any) => _.pick(i, ...$select)) + : _.pick(response, ...$select) + : response + + return toReturn as Result | Result[] + } catch (error) { + throw errorHandler(error, params) + } + } + + async remove(id: Id, params?: Params): Promise + async remove(id: null, params?: Params): Promise + async remove(id: Id | null, params?: Params): Promise { + const originalData = + id === null ? await this.find({ ...params, paginate: false }) : await this.get(id, params) + const { name } = this.options + const { Model = this.options.Model } = params || {} + + const q = Model.deleteFrom(name as any) + const convertedQuery = id === null ? params.query : { [this.id]: id } + const qWhere = this.applyWhere(q as any, convertedQuery as any) + const request = id === null ? qWhere.execute() : qWhere.executeTakeFirst() + try { + const result = await request + + if (!result) throw new NotFound(`No record found for id '${id}'`) + + return originalData as Result | Result[] + } catch (error) { + throw errorHandler(error, params) + } + } +} diff --git a/packages/kysely/test/index.test.ts b/packages/kysely/test/index.test.ts new file mode 100644 index 0000000..9b3fe53 --- /dev/null +++ b/packages/kysely/test/index.test.ts @@ -0,0 +1,99 @@ +import { describe, it } from 'node:test' +import assert from 'assert' +import { adapterTests, Person } from '@wingshq/adapter-tests' +import { Kysely } from 'kysely' + +import { KyselyAdapter } from '../src' + +const testSuite = adapterTests([ + '.id', + '.options', + '.get', + '.get + $select', + '.get + id + query', + '.get + NotFound', + '.get + id + query id', + '.find', + '.find + paginate + query', + '.find + $and', + '.find + $and + $or', + '.remove', + '.remove + $select', + '.remove + id + query', + '.remove + multi', + '.remove + multi no pagination', + '.remove + id + query id', + '.update', + '.update + $select', + '.update + id + query', + '.update + NotFound', + '.update + id + query id', + '.update + query + NotFound', + '.patch', + '.patch + $select', + '.patch + id + query', + '.patch multiple', + '.patch multiple no pagination', + '.patch multi query same', + '.patch multi query changed', + '.patch + query + NotFound', + '.patch + NotFound', + '.patch + id + query id', + '.create', + '.create ignores query', + '.create + $select', + '.create multi', + '.find + equal', + '.find + equal multiple', + '.find + $sort', + '.find + $limit', + '.find + $limit 0', + '.find + $skip', + '.find + $select', + '.find + $or', + '.find + $in', + '.find + $nin', + '.find + $lt', + '.find + $lte', + '.find + $gt', + '.find + $gte', + '.find + $ne', + '.find + $gt + $lt + $sort', + '.find + $or nested + $sort', + '.find + paginate', + '.find + paginate + $limit + $skip', + '.find + paginate + $limit 0', + '.find + paginate + params' +]) + +interface Person { + id: number + name: string + age: number +} + +interface Tables { + people: Person +} + +const db = new Kysely({ + client: 'sqlite3', + connection: { + filename: ':memory:' + }, + useNullAsDefault: true +}) + +console.log(db) + +describe('Wings kysely Adapter', () => { + const peopleAdapter = new KyselyAdapter({ Model: db, name: 'people', id: 'id' }) + + peopleAdapter.find({ query: { name: { $like: '%foo' } } }) + + it('instantiated the adapter', () => { + assert.ok(peopleAdapter) + }) + + testSuite(peopleAdapter, 'id') +}) diff --git a/packages/kysely/tsconfig.json b/packages/kysely/tsconfig.json new file mode 100644 index 0000000..f8a7bc1 --- /dev/null +++ b/packages/kysely/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig", + "include": [ + "src/**/*.ts" + ], + "compilerOptions": { + "outDir": "lib" + } +} \ No newline at end of file