@remix-run/data-table-mysql 0.0.0 → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Shopify Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,90 @@
1
- # Placeholder package
1
+ # data-table-mysql
2
2
 
3
- This package is intentionally empty and published only as a temporary placeholder.
3
+ MySQL adapter for [`remix/data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table).
4
+ Use this package when you want `data-table` APIs backed by `mysql2`.
5
+
6
+ ## Features
7
+
8
+ - **Native `mysql2` Integration**: Works with `mysql2/promise` connection pools
9
+ - **Full `data-table` API Support**: Queries, relations, writes, and transactions
10
+ - **MySQL Capabilities Enabled By Default**:
11
+ - `returning: false`
12
+ - `savepoints: true`
13
+ - `upsert: true`
14
+
15
+ ## Installation
16
+
17
+ ```sh
18
+ npm i remix mysql2
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ```ts
24
+ import { createPool } from 'mysql2/promise'
25
+ import { createDatabase } from 'remix/data-table'
26
+ import { createMysqlDatabaseAdapter } from 'remix/data-table-mysql'
27
+
28
+ let pool = createPool(process.env.DATABASE_URL as string)
29
+ let db = createDatabase(createMysqlDatabaseAdapter(pool))
30
+ ```
31
+
32
+ Use `db.query(...)`, relation loading, and transactions from `remix/data-table`.
33
+
34
+ ## Adapter Capabilities
35
+
36
+ `data-table-mysql` reports this capability set by default:
37
+
38
+ - `returning: false`
39
+ - `savepoints: true`
40
+ - `upsert: true`
41
+
42
+ ## Advanced Usage
43
+
44
+ ### Capability Overrides For Testing
45
+
46
+ Capability overrides are mainly for tests where you want to force or disable specific behavior
47
+ checks. In production, keep defaults so adapter behavior matches MySQL behavior.
48
+
49
+ ```ts
50
+ import { createMysqlDatabaseAdapter } from 'remix/data-table-mysql'
51
+
52
+ let adapter = createMysqlDatabaseAdapter(pool, {
53
+ capabilities: {
54
+ upsert: false,
55
+ },
56
+ })
57
+ ```
58
+
59
+ ### `returning` On MySQL
60
+
61
+ MySQL does not natively support SQL `RETURNING`. In this adapter, using `returning` on write
62
+ operations throws `DataTableQueryError`.
63
+
64
+ Use write metadata (`affectedRows`, `insertId`) on MySQL, or switch adapters when returned rows
65
+ are required.
66
+
67
+ ```ts
68
+ import { DataTableQueryError } from 'remix/data-table'
69
+
70
+ try {
71
+ await db
72
+ .query(Accounts)
73
+ .insert({ email: 'a@example.com', status: 'active' }, { returning: ['id'] })
74
+ } catch (error) {
75
+ if (error instanceof DataTableQueryError) {
76
+ // insert() returning is not supported by this adapter
77
+ }
78
+ }
79
+ ```
80
+
81
+ ## Related Packages
82
+
83
+ - [`data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table) - Core query/relations API
84
+ - [`data-schema`](https://github.com/remix-run/remix/tree/main/packages/data-schema) - Schema definitions and validation
85
+ - [`data-table-postgres`](https://github.com/remix-run/remix/tree/main/packages/data-table-postgres) - PostgreSQL adapter
86
+ - [`data-table-sqlite`](https://github.com/remix-run/remix/tree/main/packages/data-table-sqlite) - SQLite adapter
87
+
88
+ ## License
89
+
90
+ See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
@@ -0,0 +1,3 @@
1
+ export type { MysqlDatabaseAdapterOptions, MysqlDatabaseConnection, MysqlDatabasePool, MysqlQueryResponse, MysqlQueryResultHeader, MysqlQueryRows, } from './lib/adapter.ts';
2
+ export { createMysqlDatabaseAdapter, MysqlDatabaseAdapter } from './lib/adapter.ts';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,2BAA2B,EAC3B,uBAAuB,EACvB,iBAAiB,EACjB,kBAAkB,EAClB,sBAAsB,EACtB,cAAc,GACf,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,0BAA0B,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { createMysqlDatabaseAdapter, MysqlDatabaseAdapter } from "./lib/adapter.js";
@@ -0,0 +1,69 @@
1
+ import type { AdapterCapabilityOverrides, AdapterExecuteRequest, AdapterResult, DatabaseAdapter, TransactionOptions, TransactionToken } from '@remix-run/data-table';
2
+ /**
3
+ * Row-array response shape for mysql query calls.
4
+ */
5
+ export type MysqlQueryRows = Record<string, unknown>[];
6
+ /**
7
+ * Metadata shape for mysql write results.
8
+ */
9
+ export type MysqlQueryResultHeader = {
10
+ affectedRows: number;
11
+ insertId: unknown;
12
+ };
13
+ /**
14
+ * Supported mysql `query()` response tuple.
15
+ */
16
+ export type MysqlQueryResponse = [result: unknown, fields?: unknown];
17
+ /**
18
+ * Single mysql connection contract used by this adapter.
19
+ */
20
+ export type MysqlDatabaseConnection = {
21
+ query(text: string, values?: unknown[]): Promise<MysqlQueryResponse>;
22
+ beginTransaction(): Promise<void>;
23
+ commit(): Promise<void>;
24
+ rollback(): Promise<void>;
25
+ release?: () => void;
26
+ };
27
+ /**
28
+ * Mysql pool contract used by this adapter.
29
+ */
30
+ export type MysqlDatabasePool = {
31
+ query(text: string, values?: unknown[]): Promise<MysqlQueryResponse>;
32
+ getConnection(): Promise<MysqlDatabaseConnection>;
33
+ };
34
+ /**
35
+ * Mysql adapter configuration.
36
+ */
37
+ export type MysqlDatabaseAdapterOptions = {
38
+ capabilities?: AdapterCapabilityOverrides;
39
+ };
40
+ type MysqlQueryable = MysqlDatabasePool | MysqlDatabaseConnection;
41
+ /**
42
+ * `DatabaseAdapter` implementation for mysql-compatible clients.
43
+ */
44
+ export declare class MysqlDatabaseAdapter implements DatabaseAdapter {
45
+ #private;
46
+ dialect: string;
47
+ capabilities: {
48
+ returning: boolean;
49
+ savepoints: boolean;
50
+ upsert: boolean;
51
+ };
52
+ constructor(client: MysqlQueryable, options?: MysqlDatabaseAdapterOptions);
53
+ execute(request: AdapterExecuteRequest): Promise<AdapterResult>;
54
+ beginTransaction(options?: TransactionOptions): Promise<TransactionToken>;
55
+ commitTransaction(token: TransactionToken): Promise<void>;
56
+ rollbackTransaction(token: TransactionToken): Promise<void>;
57
+ createSavepoint(token: TransactionToken, name: string): Promise<void>;
58
+ rollbackToSavepoint(token: TransactionToken, name: string): Promise<void>;
59
+ releaseSavepoint(token: TransactionToken, name: string): Promise<void>;
60
+ }
61
+ /**
62
+ * Creates a mysql `DatabaseAdapter`.
63
+ * @param client Mysql pool or connection.
64
+ * @param options Optional adapter capability overrides.
65
+ * @returns A configured mysql adapter.
66
+ */
67
+ export declare function createMysqlDatabaseAdapter(client: MysqlQueryable, options?: MysqlDatabaseAdapterOptions): MysqlDatabaseAdapter;
68
+ export {};
69
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/lib/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,0BAA0B,EAC1B,qBAAqB,EACrB,aAAa,EACb,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EACjB,MAAM,uBAAuB,CAAA;AAK9B;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;AAEtD;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,OAAO,CAAA;CAClB,CAAA;AAED;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,CAAA;AAEpE;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAA;IACpE,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACjC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACvB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACzB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACrB,CAAA;AAED;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAA;IACpE,aAAa,IAAI,OAAO,CAAC,uBAAuB,CAAC,CAAA;CAClD,CAAA;AAED;;GAEG;AACH,MAAM,MAAM,2BAA2B,GAAG;IACxC,YAAY,CAAC,EAAE,0BAA0B,CAAA;CAC1C,CAAA;AAOD,KAAK,cAAc,GAAG,iBAAiB,GAAG,uBAAuB,CAAA;AAEjE;;GAEG;AACH,qBAAa,oBAAqB,YAAW,eAAe;;IAC1D,OAAO,SAAU;IACjB,YAAY;;;;MAAA;IAMZ,YAAY,MAAM,EAAE,cAAc,EAAE,OAAO,CAAC,EAAE,2BAA2B,EAOxE;IAEK,OAAO,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,aAAa,CAAC,CA6BpE;IAEK,gBAAgB,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAgC9E;IAEK,iBAAiB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB9D;IAEK,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBhE;IAEK,eAAe,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG1E;IAEK,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG9E;IAEK,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG3E;CAmBF;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,cAAc,EACtB,OAAO,CAAC,EAAE,2BAA2B,GACpC,oBAAoB,CAEtB"}
@@ -0,0 +1,194 @@
1
+ import { getTablePrimaryKey } from '@remix-run/data-table';
2
+ import { compileMysqlStatement } from "./sql-compiler.js";
3
+ /**
4
+ * `DatabaseAdapter` implementation for mysql-compatible clients.
5
+ */
6
+ export class MysqlDatabaseAdapter {
7
+ dialect = 'mysql';
8
+ capabilities;
9
+ #client;
10
+ #transactions = new Map();
11
+ #transactionCounter = 0;
12
+ constructor(client, options) {
13
+ this.#client = client;
14
+ this.capabilities = {
15
+ returning: options?.capabilities?.returning ?? false,
16
+ savepoints: options?.capabilities?.savepoints ?? true,
17
+ upsert: options?.capabilities?.upsert ?? true,
18
+ };
19
+ }
20
+ async execute(request) {
21
+ if (request.statement.kind === 'insertMany' && request.statement.values.length === 0) {
22
+ return {
23
+ affectedRows: 0,
24
+ insertId: undefined,
25
+ rows: request.statement.returning ? [] : undefined,
26
+ };
27
+ }
28
+ let statement = compileMysqlStatement(request.statement);
29
+ let client = this.#resolveClient(request.transaction);
30
+ let [result] = await client.query(statement.text, statement.values);
31
+ if (isRowsResult(result)) {
32
+ let rows = normalizeRows(result);
33
+ if (request.statement.kind === 'count' || request.statement.kind === 'exists') {
34
+ rows = normalizeCountRows(rows);
35
+ }
36
+ return { rows };
37
+ }
38
+ let header = normalizeHeader(result);
39
+ return {
40
+ affectedRows: header.affectedRows,
41
+ insertId: normalizeInsertId(request.statement.kind, request.statement, header),
42
+ };
43
+ }
44
+ async beginTransaction(options) {
45
+ let releaseOnClose = false;
46
+ let connection;
47
+ if (isMysqlPool(this.#client)) {
48
+ connection = await this.#client.getConnection();
49
+ releaseOnClose = true;
50
+ }
51
+ else {
52
+ connection = this.#client;
53
+ }
54
+ if (options?.isolationLevel) {
55
+ await connection.query('set transaction isolation level ' + options.isolationLevel);
56
+ }
57
+ if (options?.readOnly !== undefined) {
58
+ await connection.query(options.readOnly ? 'set transaction read only' : 'set transaction read write');
59
+ }
60
+ await connection.beginTransaction();
61
+ this.#transactionCounter += 1;
62
+ let token = { id: 'tx_' + String(this.#transactionCounter) };
63
+ this.#transactions.set(token.id, {
64
+ connection,
65
+ releaseOnClose,
66
+ });
67
+ return token;
68
+ }
69
+ async commitTransaction(token) {
70
+ let transaction = this.#transactions.get(token.id);
71
+ if (!transaction) {
72
+ throw new Error('Unknown transaction token: ' + token.id);
73
+ }
74
+ try {
75
+ await transaction.connection.commit();
76
+ }
77
+ finally {
78
+ this.#transactions.delete(token.id);
79
+ if (transaction.releaseOnClose) {
80
+ transaction.connection.release?.();
81
+ }
82
+ }
83
+ }
84
+ async rollbackTransaction(token) {
85
+ let transaction = this.#transactions.get(token.id);
86
+ if (!transaction) {
87
+ throw new Error('Unknown transaction token: ' + token.id);
88
+ }
89
+ try {
90
+ await transaction.connection.rollback();
91
+ }
92
+ finally {
93
+ this.#transactions.delete(token.id);
94
+ if (transaction.releaseOnClose) {
95
+ transaction.connection.release?.();
96
+ }
97
+ }
98
+ }
99
+ async createSavepoint(token, name) {
100
+ let connection = this.#transactionConnection(token);
101
+ await connection.query('savepoint ' + quoteIdentifier(name));
102
+ }
103
+ async rollbackToSavepoint(token, name) {
104
+ let connection = this.#transactionConnection(token);
105
+ await connection.query('rollback to savepoint ' + quoteIdentifier(name));
106
+ }
107
+ async releaseSavepoint(token, name) {
108
+ let connection = this.#transactionConnection(token);
109
+ await connection.query('release savepoint ' + quoteIdentifier(name));
110
+ }
111
+ #resolveClient(token) {
112
+ if (!token) {
113
+ return this.#client;
114
+ }
115
+ return this.#transactionConnection(token);
116
+ }
117
+ #transactionConnection(token) {
118
+ let transaction = this.#transactions.get(token.id);
119
+ if (!transaction) {
120
+ throw new Error('Unknown transaction token: ' + token.id);
121
+ }
122
+ return transaction.connection;
123
+ }
124
+ }
125
+ /**
126
+ * Creates a mysql `DatabaseAdapter`.
127
+ * @param client Mysql pool or connection.
128
+ * @param options Optional adapter capability overrides.
129
+ * @returns A configured mysql adapter.
130
+ */
131
+ export function createMysqlDatabaseAdapter(client, options) {
132
+ return new MysqlDatabaseAdapter(client, options);
133
+ }
134
+ function isMysqlPool(client) {
135
+ return typeof client.getConnection === 'function';
136
+ }
137
+ function isRowsResult(result) {
138
+ return Array.isArray(result) && (result.length === 0 || !Array.isArray(result[0]));
139
+ }
140
+ function normalizeRows(rows) {
141
+ return rows.map((row) => ({ ...row }));
142
+ }
143
+ function normalizeHeader(result) {
144
+ if (typeof result === 'object' && result !== null) {
145
+ let header = result;
146
+ return {
147
+ affectedRows: typeof header.affectedRows === 'number' ? header.affectedRows : 0,
148
+ insertId: header.insertId,
149
+ };
150
+ }
151
+ return {
152
+ affectedRows: 0,
153
+ insertId: undefined,
154
+ };
155
+ }
156
+ function normalizeCountRows(rows) {
157
+ return rows.map((row) => {
158
+ let count = row.count;
159
+ if (typeof count === 'string') {
160
+ let numeric = Number(count);
161
+ if (!Number.isNaN(numeric)) {
162
+ return {
163
+ ...row,
164
+ count: numeric,
165
+ };
166
+ }
167
+ }
168
+ if (typeof count === 'bigint') {
169
+ return {
170
+ ...row,
171
+ count: Number(count),
172
+ };
173
+ }
174
+ return row;
175
+ });
176
+ }
177
+ function normalizeInsertId(kind, statement, header) {
178
+ if (!isInsertStatementKind(kind) || !isInsertStatement(statement)) {
179
+ return undefined;
180
+ }
181
+ if (getTablePrimaryKey(statement.table).length !== 1) {
182
+ return undefined;
183
+ }
184
+ return header.insertId;
185
+ }
186
+ function quoteIdentifier(value) {
187
+ return '`' + value.replace(/`/g, '``') + '`';
188
+ }
189
+ function isInsertStatementKind(kind) {
190
+ return kind === 'insert' || kind === 'insertMany' || kind === 'upsert';
191
+ }
192
+ function isInsertStatement(statement) {
193
+ return (statement.kind === 'insert' || statement.kind === 'insertMany' || statement.kind === 'upsert');
194
+ }
@@ -0,0 +1,8 @@
1
+ import type { AdapterStatement } from '@remix-run/data-table';
2
+ type CompiledSql = {
3
+ text: string;
4
+ values: unknown[];
5
+ };
6
+ export declare function compileMysqlStatement(statement: AdapterStatement): CompiledSql;
7
+ export {};
8
+ //# sourceMappingURL=sql-compiler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql-compiler.d.ts","sourceRoot":"","sources":["../../src/lib/sql-compiler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAa,MAAM,uBAAuB,CAAA;AAMxE,KAAK,WAAW,GAAG;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,OAAO,EAAE,CAAA;CAClB,CAAA;AAMD,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,gBAAgB,GAAG,WAAW,CAgG9E"}