Skip to content

Commit 82db3c5

Browse files
committed
Basics working
0 parents  commit 82db3c5

14 files changed

Lines changed: 3170 additions & 0 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
dist

jest.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
transform: {
3+
"\\.ts$": "esbuild-runner/jest",
4+
},
5+
};

module-fixup.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
cat > dist/cjs/package.json <<!EOF
2+
{
3+
"type": "commonjs"
4+
}
5+
!EOF
6+
7+
cat > dist/esm/package.json <<!EOF
8+
{
9+
"type": "module"
10+
}
11+
!EOF

package.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"name": "kysely-data-api",
3+
"version": "0.0.1",
4+
"repository": "git@github.com:serverless-stack/kysely-data-api.git",
5+
"main": "dist/cjs/index.js",
6+
"module": "dist/esm/index.js",
7+
"exports": {
8+
".": {
9+
"import": "./dist/esm/index.js",
10+
"require": "./dist/cjs/index.js"
11+
}
12+
},
13+
"license": "MIT",
14+
"scripts": {
15+
"clean": "rm -rf dist && rm -rf test/dist",
16+
"build": "yarn clean && yarn build:esm && yarn build:cjs && ./module-fixup.sh",
17+
"build:esm": "tsc -p tsconfig-esm.json",
18+
"build:cjs": "tsc -p tsconfig-cjs.json"
19+
},
20+
"devDependencies": {
21+
"@tsconfig/node14": "^1.0.1",
22+
"@types/jest": "^27.0.2",
23+
"@types/node": "^16.11.0",
24+
"aws-sdk": "^2.1008.0",
25+
"esbuild": "^0.13.6",
26+
"esbuild-runner": "^2.2.1",
27+
"jest": "^27.2.5",
28+
"kysely": "^0.6.2"
29+
},
30+
"peerDependencies": {
31+
"aws-sdk": "^2.1008.0",
32+
"kysely": "^0.6.2"
33+
}
34+
}

src/data-api-dialect.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Driver } from "kysely";
2+
import { Kysely } from "kysely";
3+
import { QueryCompiler } from "kysely";
4+
import { Dialect } from "kysely";
5+
import { DatabaseIntrospector } from "kysely";
6+
import { PostgresIntrospector } from "kysely";
7+
import { DataApiDriver, DataApiDriverConfig } from "./data-api-driver";
8+
import { DataApiQueryCompiler } from "./data-api-query-compiler";
9+
10+
type DataApiDialectConfig = {
11+
driver: DataApiDriverConfig;
12+
};
13+
14+
export class DataApiDialect implements Dialect {
15+
#config: DataApiDialectConfig;
16+
17+
constructor(config: DataApiDialectConfig) {
18+
this.#config = config;
19+
}
20+
21+
createDriver(): Driver {
22+
return new DataApiDriver(this.#config.driver);
23+
}
24+
25+
createQueryCompiler(): QueryCompiler {
26+
// The default query compiler is for postgres dialect.
27+
return new DataApiQueryCompiler();
28+
}
29+
30+
createIntrospector(db: Kysely<any>): DatabaseIntrospector {
31+
console.log("Introspector");
32+
return new PostgresIntrospector(db);
33+
}
34+
}

src/data-api-driver.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { DatabaseConnection, QueryResult } from "kysely";
2+
import { Driver } from "kysely";
3+
import { CompiledQuery } from "kysely";
4+
import * as RDSDataService from "aws-sdk/clients/rdsdataservice";
5+
6+
export type DataApiDriverConfig = {
7+
client: RDSDataService;
8+
secretArn: string;
9+
resourceArn: string;
10+
database: string;
11+
};
12+
13+
export class DataApiDriver extends Driver {
14+
#config: DataApiDriverConfig;
15+
16+
constructor(config: DataApiDriverConfig) {
17+
super();
18+
this.#config = config;
19+
}
20+
21+
protected override async init(): Promise<void> {}
22+
23+
protected override async acquireConnection(): Promise<DatabaseConnection> {
24+
return new DataApiConnection(this.#config);
25+
}
26+
27+
override async beginTransaction(conn: DataApiConnection) {
28+
await conn.beginTransaction();
29+
}
30+
31+
override async commitTransaction(conn: DataApiConnection) {
32+
await conn.commitTransaction();
33+
}
34+
35+
override async rollbackTransaction(conn: DataApiConnection) {
36+
await conn.rollbackTransaction();
37+
}
38+
39+
protected override async releaseConnection(
40+
_connection: DatabaseConnection
41+
): Promise<void> {}
42+
43+
protected override async destroy(): Promise<void> {}
44+
}
45+
46+
class DataApiConnection implements DatabaseConnection {
47+
#config: DataApiDriverConfig;
48+
#transactionId?: string;
49+
50+
constructor(config: DataApiDriverConfig) {
51+
this.#config = config;
52+
}
53+
54+
async beginTransaction() {
55+
const r = await this.#config.client
56+
.beginTransaction({
57+
secretArn: this.#config.secretArn,
58+
resourceArn: this.#config.resourceArn,
59+
database: this.#config.database,
60+
})
61+
.promise();
62+
this.#transactionId = r.transactionId;
63+
}
64+
65+
async commitTransaction() {
66+
if (!this.#transactionId)
67+
throw new Error("Cannot commit a transaction before creating it");
68+
await this.#config.client
69+
.commitTransaction({
70+
secretArn: this.#config.secretArn,
71+
resourceArn: this.#config.resourceArn,
72+
transactionId: this.#transactionId,
73+
})
74+
.promise();
75+
}
76+
77+
async rollbackTransaction() {
78+
if (!this.#transactionId)
79+
throw new Error("Cannot rollback a transaction before creating it");
80+
await this.#config.client
81+
.rollbackTransaction({
82+
secretArn: this.#config.secretArn,
83+
resourceArn: this.#config.resourceArn,
84+
transactionId: this.#transactionId,
85+
})
86+
.promise();
87+
}
88+
89+
async executeQuery<O>(compiledQuery: CompiledQuery): Promise<QueryResult<O>> {
90+
const r = await this.#config.client
91+
.executeStatement({
92+
transactionId: this.#transactionId,
93+
secretArn: this.#config.secretArn,
94+
resourceArn: this.#config.resourceArn,
95+
sql: compiledQuery.sql,
96+
parameters: compiledQuery.bindings as any,
97+
database: this.#config.database,
98+
includeResultMetadata: true,
99+
})
100+
.promise();
101+
if (!r.columnMetadata) {
102+
return {
103+
numUpdatedOrDeletedRows: r.numberOfRecordsUpdated,
104+
};
105+
}
106+
const rows = r.records
107+
?.filter((r) => r.length !== 0)
108+
.map(
109+
(rec) =>
110+
Object.fromEntries(
111+
rec.map((val, i) => [
112+
r.columnMetadata![i].name,
113+
val.stringValue ||
114+
val.blobValue ||
115+
val.longValue ||
116+
val.arrayValue ||
117+
val.doubleValue ||
118+
(val.isNull ? null : val.booleanValue),
119+
])
120+
) as O
121+
);
122+
const result: QueryResult<O> = {
123+
rows,
124+
};
125+
return result;
126+
}
127+
}

src/data-api-query-compiler.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Field } from "aws-sdk/clients/rdsdataservice";
2+
import { DefaultQueryCompiler } from "kysely";
3+
4+
export class DataApiQueryCompiler extends DefaultQueryCompiler {
5+
protected override appendValue(value: any) {
6+
const name = this.numBindings;
7+
this.append(this.getCurrentParameterPlaceholder());
8+
this.addBinding({
9+
name: name.toString(),
10+
value: serialize(value),
11+
});
12+
}
13+
14+
protected override getCurrentParameterPlaceholder() {
15+
return ":" + this.numBindings;
16+
}
17+
}
18+
19+
function serialize(value: any): Field {
20+
if (value == null) return { isNull: true };
21+
switch (typeof value) {
22+
case "number":
23+
return {
24+
longValue: value,
25+
};
26+
case "bigint":
27+
return {
28+
doubleValue: Number(value),
29+
};
30+
case "string":
31+
return {
32+
stringValue: value,
33+
};
34+
case "boolean":
35+
return {
36+
booleanValue: value,
37+
};
38+
}
39+
40+
throw "wtf";
41+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./data-api-dialect";

test/harness.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import RDSDataService from "aws-sdk/clients/rdsdataservice";
2+
import { Kysely } from "kysely";
3+
import { DataApiDialect } from "../src";
4+
import { DataApiDriverConfig } from "../src/data-api-driver";
5+
6+
const TEST_DATABASE = "scratch";
7+
8+
const opts: DataApiDriverConfig = {
9+
client: new RDSDataService(),
10+
database: TEST_DATABASE,
11+
secretArn: process.env.RDS_SECRET,
12+
resourceArn: process.env.RDS_ARN,
13+
};
14+
const dialect = new DataApiDialect({
15+
driver: opts,
16+
});
17+
18+
interface Person {
19+
id: number;
20+
first_name: string;
21+
last_name: string;
22+
gender: "male" | "female" | "other";
23+
}
24+
25+
interface Pet {
26+
id: number;
27+
name: string;
28+
owner_id: number;
29+
species: "dog" | "cat";
30+
}
31+
32+
interface Movie {
33+
id: string;
34+
stars: number;
35+
}
36+
37+
// Keys are table names.
38+
interface Database {
39+
person: Person;
40+
pet: Pet;
41+
movie: Movie;
42+
}
43+
44+
export const db = new Kysely<Database>({ dialect });
45+
46+
export async function reset() {
47+
await opts.client
48+
.executeStatement({
49+
sql: `
50+
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pid <> pg_backend_pid() AND datname = '${TEST_DATABASE}'`,
51+
database: "postgres",
52+
secretArn: opts.secretArn,
53+
resourceArn: opts.resourceArn,
54+
})
55+
.promise();
56+
57+
await opts.client
58+
.executeStatement({
59+
sql: `DROP DATABASE IF EXISTS ${TEST_DATABASE}`,
60+
database: "postgres",
61+
secretArn: opts.secretArn,
62+
resourceArn: opts.resourceArn,
63+
})
64+
.promise();
65+
66+
await opts.client
67+
.executeStatement({
68+
sql: `CREATE DATABASE ${TEST_DATABASE}`,
69+
database: "postgres",
70+
secretArn: opts.secretArn,
71+
resourceArn: opts.resourceArn,
72+
})
73+
.promise();
74+
75+
await db.schema
76+
.createTable("person")
77+
.addColumn("id", "integer", (col) => col.increments().primaryKey())
78+
.addColumn("first_name", "varchar")
79+
.addColumn("last_name", "varchar")
80+
.addColumn("gender", "varchar(50)")
81+
.execute();
82+
83+
await db.schema
84+
.createTable("pet")
85+
.addColumn("id", "integer", (col) => col.increments().primaryKey())
86+
.addColumn("name", "varchar", (col) => col.notNull().unique())
87+
.addColumn("owner_id", "integer", (col) =>
88+
col.references("person.id").onDelete("cascade")
89+
)
90+
.addColumn("species", "varchar")
91+
.execute();
92+
93+
await db.schema
94+
.createIndex("pet_owner_id_index")
95+
.on("pet")
96+
.column("owner_id")
97+
.execute();
98+
}

0 commit comments

Comments
 (0)