@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 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,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "checkJs": true,
4
+ "moduleResolution": "NodeNext",
5
+ "module": "NodeNext",
6
+ "composite": true,
7
+ },
8
+ "include": [
9
+ "*",
10
+ "src/*",
11
+ "tests/*.js",
12
+ "d1/*"
13
+ ]
14
+ }
@@ -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
@@ -0,0 +1,5 @@
1
+ export { migrateToLatest } from '@storecraft/database-sql-base/migrate.js';
2
+
3
+
4
+
5
+
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();
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
+ }