@storecraft/database-planetscale 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -0
- package/index.js +34 -0
- package/jsconfig.json +14 -0
- package/kysely.planet.dialect.js +209 -0
- package/migrate.js +5 -0
- package/package.json +40 -0
- package/tests/runner.test.js +44 -0
- package/tests/sandbox.js +0 -0
- package/types.public.d.ts +18 -0
package/README.md
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
# Storecraft Planetscale (cloud mysql) Database support
|
2
|
+
|
3
|
+
<div style="text-align:center">
|
4
|
+
<img src='https://storecraft.app/storecraft-color.svg'
|
5
|
+
width='90%'' />
|
6
|
+
</div><hr/><br/>
|
7
|
+
|
8
|
+
Official `Planetscale` (cloud MySql) driver for `StoreCraft` on any platforms.
|
9
|
+
|
10
|
+
```bash
|
11
|
+
npm i @storecraft/database-neon
|
12
|
+
```
|
13
|
+
|
14
|
+
## Setup
|
15
|
+
|
16
|
+
- First, login to your [planetscale](https://planetscale.com/) account.
|
17
|
+
- Create a database.
|
18
|
+
- Copy the `connection string`.
|
19
|
+
|
20
|
+
|
21
|
+
## usage
|
22
|
+
|
23
|
+
```js
|
24
|
+
import 'dotenv/config';
|
25
|
+
import http from "node:http";
|
26
|
+
import { join } from "node:path";
|
27
|
+
import { homedir } from "node:os";
|
28
|
+
|
29
|
+
import { App } from '@storecraft/core'
|
30
|
+
import { NodePlatform } from '@storecraft/platforms/node';
|
31
|
+
import { PlanetScale } from '@storecraft/database-planetscale'
|
32
|
+
import { migrateToLatest } from '@storecraft/database-planetscale/migrate.js'
|
33
|
+
import { NodeLocalStorage } from '@storecraft/storage-local/node'
|
34
|
+
|
35
|
+
|
36
|
+
const app = new App(
|
37
|
+
{
|
38
|
+
auth_admins_emails: ['admin@sc.com'],
|
39
|
+
auth_secret_access_token: 'auth_secret_access_token',
|
40
|
+
auth_secret_refresh_token: 'auth_secret_refresh_token'
|
41
|
+
}
|
42
|
+
)
|
43
|
+
.withPlatform(new NodePlatform())
|
44
|
+
.withDatabase(
|
45
|
+
new PlanetScale(
|
46
|
+
{
|
47
|
+
url: process.env.PLANETSCALE_CONNECTION_URL,
|
48
|
+
useSharedConnection: true
|
49
|
+
}
|
50
|
+
)
|
51
|
+
)
|
52
|
+
.withStorage(new NodeLocalStorage(join(homedir(), 'tomer')))
|
53
|
+
|
54
|
+
await app.init();
|
55
|
+
await migrateToLatest(app.db, false);
|
56
|
+
|
57
|
+
const server = http.createServer(app.handler).listen(
|
58
|
+
8000,
|
59
|
+
() => {
|
60
|
+
console.log(`Server is running on http://localhost:8000`);
|
61
|
+
}
|
62
|
+
);
|
63
|
+
|
64
|
+
```
|
65
|
+
|
66
|
+
```text
|
67
|
+
Author: Tomer Shalev <tomer.shalev@gmail.com>
|
68
|
+
```
|
package/index.js
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
import { SQL } from '@storecraft/database-sql-base';
|
2
|
+
import { PlanetScaleDialect } from './kysely.planet.dialect.js';
|
3
|
+
|
4
|
+
|
5
|
+
/**
|
6
|
+
* @param {any} b
|
7
|
+
* @param {string} msg
|
8
|
+
*/
|
9
|
+
const assert = (b, msg) => {
|
10
|
+
if(!Boolean(b)) throw new Error(msg);
|
11
|
+
}
|
12
|
+
|
13
|
+
|
14
|
+
/**
|
15
|
+
*
|
16
|
+
* @extends {SQL}
|
17
|
+
*/
|
18
|
+
export class PlanetScale extends SQL {
|
19
|
+
|
20
|
+
/**
|
21
|
+
*
|
22
|
+
* @param {import('./types.public.d.ts').PlanetScaleDialectConfig} [config] config
|
23
|
+
*/
|
24
|
+
constructor(config) {
|
25
|
+
super(
|
26
|
+
{
|
27
|
+
dialect_type: 'MYSQL',
|
28
|
+
dialect: new PlanetScaleDialect(config),
|
29
|
+
}
|
30
|
+
);
|
31
|
+
|
32
|
+
}
|
33
|
+
|
34
|
+
}
|
package/jsconfig.json
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
import {connect, Connection} from '@planetscale/database'
|
2
|
+
import {
|
3
|
+
CompiledQuery,
|
4
|
+
Kysely,
|
5
|
+
MysqlAdapter,
|
6
|
+
MysqlIntrospector,
|
7
|
+
MysqlQueryCompiler,
|
8
|
+
} from 'kysely'
|
9
|
+
|
10
|
+
/**
|
11
|
+
* @typedef {import('kysely').Dialect} Dialect
|
12
|
+
* @typedef {import('kysely').Driver} Driver
|
13
|
+
* @typedef {import('kysely').DatabaseConnection} DatabaseConnection
|
14
|
+
* @typedef {import('kysely').DatabaseIntrospector} DatabaseIntrospector
|
15
|
+
* @typedef {import('./types.public.d.ts').PlanetScaleDialectConfig} PlanetScaleDialectConfig
|
16
|
+
*/
|
17
|
+
|
18
|
+
/**
|
19
|
+
* PlanetScale dialect that uses the [PlanetScale Serverless Driver for JavaScript][0].
|
20
|
+
* The constructor takes an instance of {@link Config} from `@planetscale/database`.
|
21
|
+
*
|
22
|
+
* ```typescript
|
23
|
+
* new PlanetScaleDialect({
|
24
|
+
* host: '<host>',
|
25
|
+
* username: '<username>',
|
26
|
+
* password: '<password>',
|
27
|
+
* })
|
28
|
+
*
|
29
|
+
* // or with a connection URL
|
30
|
+
*
|
31
|
+
* new PlanetScaleDialect({
|
32
|
+
* url: process.env.DATABASE_URL ?? 'mysql://<username>:<password>@<host>/<database>'
|
33
|
+
* })
|
34
|
+
* ```
|
35
|
+
*
|
36
|
+
* See the [`@planetscale/database` documentation][1] for more information.
|
37
|
+
*
|
38
|
+
* [0]: https://github.com/planetscale/database-js
|
39
|
+
* [1]: https://github.com/planetscale/database-js#readme
|
40
|
+
*
|
41
|
+
* @implements {Dialect}
|
42
|
+
*/
|
43
|
+
export class PlanetScaleDialect {
|
44
|
+
|
45
|
+
/**
|
46
|
+
* @param {PlanetScaleDialectConfig} config
|
47
|
+
*/
|
48
|
+
constructor(config) {
|
49
|
+
this.config = config
|
50
|
+
}
|
51
|
+
|
52
|
+
createAdapter() { return new MysqlAdapter() }
|
53
|
+
createDriver() { return new PlanetScaleDriver(this.config) }
|
54
|
+
createQueryCompiler() { return new MysqlQueryCompiler() }
|
55
|
+
/** @type {Dialect["createIntrospector"]} */
|
56
|
+
createIntrospector(db) { return new MysqlIntrospector(db) }
|
57
|
+
}
|
58
|
+
|
59
|
+
/**
|
60
|
+
* @implements {Driver}
|
61
|
+
*/
|
62
|
+
class PlanetScaleDriver {
|
63
|
+
|
64
|
+
/**
|
65
|
+
* @param {PlanetScaleDialectConfig} config
|
66
|
+
*/
|
67
|
+
constructor(config) {
|
68
|
+
this.config = config
|
69
|
+
}
|
70
|
+
|
71
|
+
async init() {}
|
72
|
+
|
73
|
+
async acquireConnection(){
|
74
|
+
return new PlanetScaleConnection(this.config)
|
75
|
+
}
|
76
|
+
|
77
|
+
/**
|
78
|
+
* @param {PlanetScaleConnection} conn
|
79
|
+
*/
|
80
|
+
async beginTransaction(conn) {
|
81
|
+
return await conn.beginTransaction()
|
82
|
+
}
|
83
|
+
|
84
|
+
/**
|
85
|
+
* @param {PlanetScaleConnection} conn
|
86
|
+
*/
|
87
|
+
async commitTransaction(conn) {
|
88
|
+
return await conn.commitTransaction()
|
89
|
+
}
|
90
|
+
|
91
|
+
/**
|
92
|
+
* @param {PlanetScaleConnection} conn
|
93
|
+
*/
|
94
|
+
async rollbackTransaction(conn) {
|
95
|
+
return await conn.rollbackTransaction()
|
96
|
+
}
|
97
|
+
|
98
|
+
/**
|
99
|
+
* @param {PlanetScaleConnection} _conn
|
100
|
+
*/
|
101
|
+
async releaseConnection(_conn) {}
|
102
|
+
|
103
|
+
async destroy() {}
|
104
|
+
}
|
105
|
+
|
106
|
+
|
107
|
+
/** @type {WeakMap<PlanetScaleDialectConfig, Connection>} */
|
108
|
+
const sharedConnections = new WeakMap()
|
109
|
+
|
110
|
+
|
111
|
+
/**
|
112
|
+
* @implements {DatabaseConnection}
|
113
|
+
*/
|
114
|
+
class PlanetScaleConnection {
|
115
|
+
/** @type {Connection} */
|
116
|
+
#conn;
|
117
|
+
/** @type {PlanetScaleConnection} */
|
118
|
+
#transactionClient;
|
119
|
+
|
120
|
+
/**
|
121
|
+
*
|
122
|
+
* @param {PlanetScaleDialectConfig} config
|
123
|
+
* @param {boolean} [isForTransaction=false]
|
124
|
+
*/
|
125
|
+
constructor(config, isForTransaction = false) {
|
126
|
+
this.config = config;
|
127
|
+
|
128
|
+
const useSharedConnection = config.useSharedConnection && !isForTransaction;
|
129
|
+
const sharedConnection = useSharedConnection ? sharedConnections.get(config) : undefined;
|
130
|
+
|
131
|
+
this.#conn = sharedConnection ?? connect({...config});
|
132
|
+
|
133
|
+
if (useSharedConnection)
|
134
|
+
sharedConnections.set(config, this.#conn);
|
135
|
+
}
|
136
|
+
|
137
|
+
/**
|
138
|
+
* @template R result type
|
139
|
+
*
|
140
|
+
* @param {CompiledQuery} compiledQuery
|
141
|
+
*
|
142
|
+
* @returns {Promise<import('kysely').QueryResult<R>>}
|
143
|
+
*/
|
144
|
+
async executeQuery(compiledQuery) {
|
145
|
+
if (this.#transactionClient)
|
146
|
+
return this.#transactionClient.executeQuery(compiledQuery);
|
147
|
+
|
148
|
+
// If no custom formatter is provided, format dates as DB date strings
|
149
|
+
const parameters = compiledQuery.parameters;
|
150
|
+
const results = await this.#conn.execute(
|
151
|
+
compiledQuery.sql, parameters
|
152
|
+
);
|
153
|
+
|
154
|
+
// @planetscale/database versions older than 1.3.0 return errors directly, rather than throwing
|
155
|
+
if (( /** @type {any} */ (results))?.error) {
|
156
|
+
throw ( /** @type {any} */ (results))?.error;
|
157
|
+
}
|
158
|
+
|
159
|
+
const numAffectedRows = (results.rowsAffected == null) ?
|
160
|
+
undefined : BigInt(results.rowsAffected);
|
161
|
+
const insertId = (results.insertId !== null && results.insertId.toString() !== '0') ?
|
162
|
+
BigInt(results.insertId) : undefined;
|
163
|
+
|
164
|
+
return {
|
165
|
+
insertId,
|
166
|
+
rows: ( /** @type {R[]} */ (results.rows)),
|
167
|
+
// @ts-ignore replaces `QueryResult.numUpdatedOrDeletedRows` in kysely > 0.22
|
168
|
+
// following https://github.com/koskimas/kysely/pull/188
|
169
|
+
numAffectedRows,
|
170
|
+
// deprecated in kysely > 0.22, keep for backward compatibility.
|
171
|
+
numUpdatedOrDeletedRows: numAffectedRows,
|
172
|
+
}
|
173
|
+
}
|
174
|
+
|
175
|
+
async beginTransaction() {
|
176
|
+
this.#transactionClient = this.#transactionClient ?? new PlanetScaleConnection(this.config, true);
|
177
|
+
await this.#transactionClient.#conn.execute('BEGIN');
|
178
|
+
}
|
179
|
+
|
180
|
+
async commitTransaction() {
|
181
|
+
if (!this.#transactionClient) throw new Error('No transaction to commit')
|
182
|
+
try {
|
183
|
+
await this.#transactionClient.#conn.execute('COMMIT')
|
184
|
+
} finally {
|
185
|
+
this.#transactionClient = undefined
|
186
|
+
}
|
187
|
+
}
|
188
|
+
|
189
|
+
async rollbackTransaction() {
|
190
|
+
if (!this.#transactionClient) throw new Error('No transaction to rollback')
|
191
|
+
try {
|
192
|
+
await this.#transactionClient.#conn.execute('ROLLBACK')
|
193
|
+
} finally {
|
194
|
+
this.#transactionClient = undefined
|
195
|
+
}
|
196
|
+
}
|
197
|
+
|
198
|
+
/**
|
199
|
+
* @template R result type
|
200
|
+
*
|
201
|
+
* @param {CompiledQuery} _compiledQuery
|
202
|
+
* @param {number} _chunkSize
|
203
|
+
*
|
204
|
+
* @returns {AsyncIterableIterator<import('kysely').QueryResult<R>>}
|
205
|
+
*/
|
206
|
+
async *streamQuery(_compiledQuery, _chunkSize) {
|
207
|
+
throw new Error('PlanetScale Serverless Driver does not support streaming')
|
208
|
+
}
|
209
|
+
}
|
package/migrate.js
ADDED
package/package.json
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
{
|
2
|
+
"name": "@storecraft/database-planetscale",
|
3
|
+
"version": "1.0.0",
|
4
|
+
"description": "`Storecraft` database driver for `PlanetScale` (cloud mysql)",
|
5
|
+
"license": "MIT",
|
6
|
+
"author": "Tomer Shalev (https://github.com/store-craft)",
|
7
|
+
"homepage": "https://github.com/store-craft/storecraft",
|
8
|
+
"repository": {
|
9
|
+
"type": "git",
|
10
|
+
"url": "https://github.com/store-craft/storecraft.git",
|
11
|
+
"directory": "packages/database-planetscale"
|
12
|
+
},
|
13
|
+
"keywords": [
|
14
|
+
"commerce",
|
15
|
+
"dashboard",
|
16
|
+
"code",
|
17
|
+
"storecraft"
|
18
|
+
],
|
19
|
+
"type": "module",
|
20
|
+
"main": "index.js",
|
21
|
+
"types": "./types.public.d.ts",
|
22
|
+
"scripts": {
|
23
|
+
"database-planetscale:test": "node ./tests/runner.test.js",
|
24
|
+
"database-planetscale:publish": "npm publish --access public"
|
25
|
+
},
|
26
|
+
"peerDependencies": {
|
27
|
+
"kysely": "*"
|
28
|
+
},
|
29
|
+
"dependencies": {
|
30
|
+
"@planetscale/database": "^1.18.0",
|
31
|
+
"@storecraft/core": "^1.0.0",
|
32
|
+
"@storecraft/database-sql-base": "^1.0.0"
|
33
|
+
},
|
34
|
+
"devDependencies": {
|
35
|
+
"@storecraft/test-runner": "^1.0.0",
|
36
|
+
"@types/node": "^20.11.0",
|
37
|
+
"dotenv": "^16.3.1",
|
38
|
+
"uvu": "^0.5.6"
|
39
|
+
}
|
40
|
+
}
|
@@ -0,0 +1,44 @@
|
|
1
|
+
import { App } from '@storecraft/core';
|
2
|
+
import { NodePlatform } from '@storecraft/platforms/node';
|
3
|
+
import { api_index } from '@storecraft/test-runner'
|
4
|
+
import { PlanetScale } from '@storecraft/database-planetscale';
|
5
|
+
import { migrateToLatest } from '@storecraft/database-planetscale/migrate.js';
|
6
|
+
|
7
|
+
export const create_app = async () => {
|
8
|
+
|
9
|
+
const app = new App(
|
10
|
+
{
|
11
|
+
auth_admins_emails: ['admin@sc.com'],
|
12
|
+
auth_secret_access_token: 'auth_secret_access_token',
|
13
|
+
auth_secret_refresh_token: 'auth_secret_refresh_token'
|
14
|
+
}
|
15
|
+
)
|
16
|
+
.withPlatform(new NodePlatform())
|
17
|
+
.withDatabase(
|
18
|
+
new PlanetScale(
|
19
|
+
{
|
20
|
+
url: process.env.PLANETSCALE_CONNECTION_URL,
|
21
|
+
useSharedConnection: true
|
22
|
+
}
|
23
|
+
)
|
24
|
+
)
|
25
|
+
|
26
|
+
return app.init();
|
27
|
+
}
|
28
|
+
|
29
|
+
async function test() {
|
30
|
+
const app = await create_app();
|
31
|
+
|
32
|
+
await migrateToLatest(app.db, false);
|
33
|
+
|
34
|
+
Object.entries(api_index).slice(0, -1).forEach(
|
35
|
+
([name, runner]) => {
|
36
|
+
runner.create(app).run();
|
37
|
+
}
|
38
|
+
);
|
39
|
+
const last_test = Object.values(api_index).at(-1).create(app);
|
40
|
+
last_test.after(async ()=>{app.db.disconnect()});
|
41
|
+
last_test.run();
|
42
|
+
}
|
43
|
+
|
44
|
+
test();
|
package/tests/sandbox.js
ADDED
File without changes
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import type { Config } from '@planetscale/database'
|
2
|
+
|
3
|
+
export { PlanetScale } from './index.js'
|
4
|
+
|
5
|
+
/**
|
6
|
+
* @description Config for the PlanetScale dialect. It extends {@link Config} from `@planetscale/database`,
|
7
|
+
* so you can pass any of those options to the constructor.
|
8
|
+
*
|
9
|
+
* @see https://github.com/planetscale/database-js#usage
|
10
|
+
*/
|
11
|
+
export interface PlanetScaleDialectConfig extends Config {
|
12
|
+
/**
|
13
|
+
* Use a single `@planetscale/database` connection for all non-transaction queries.
|
14
|
+
*
|
15
|
+
* @default false
|
16
|
+
*/
|
17
|
+
useSharedConnection?: boolean
|
18
|
+
}
|