@getstrata/core 1.0.1 → 1.0.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @getstrata/core changelog
2
2
 
3
+ ## 1.0.3
4
+
5
+ - `eta` and `mysql2` are optional peers, loaded with `import()` on first use. SQLite and Postgres apps no longer install `mysql2` through core. If a package is missing, the error names `bun add` for that package.
6
+ - Breaking: `createMysqlPool(url)` returns a `Promise` because it lazy-loads `mysql2`. It shipped synchronous in 1.0.1 and 1.0.2; `await` it. `createMysqlConnection(url)` stays synchronous and opens the pool on the first query.
7
+ - `createMysqlConnection()` shares one pool across concurrent first queries by caching the pending promise. Before this fix, three requests arriving together opened three pools and `close()` ended only the last one. A failed load is retried on the next query, and a query after `close()` opens a fresh pool.
8
+ - The MySQL helpers stay exported from `@getstrata/core` and `@getstrata/core/database`. Importing either barrel does not install `mysql2`; the barrel bundle carries the lazy `import("mysql2/promise")` and evaluates it only when a MySQL pool is opened.
9
+
10
+ ## 1.0.2
11
+
12
+ - Lockstep with `create-strata` 1.0.2. No runtime changes.
13
+
3
14
  ## 1.0.1
4
15
 
5
16
  - Publish the `contracts/authUserDirectory` subpath. Generated cookie and token apps import `AuthUserDirectory` from it, and without the export their `tsc --noEmit` failed with TS2307.
package/README.md CHANGED
@@ -39,7 +39,7 @@ The engine is your choice, and an app should have one primary engine. Postgres a
39
39
 
40
40
  Full-text `tsMatch` is PostgreSQL only.
41
41
 
42
- `mysql2` and `eta` are direct dependencies, so they install even for a SQLite JSON API app.
42
+ `eta` and `mysql2` are optional peers. Core loads them with `import()` the first time you render a view or open a MySQL connection. SQLite and Postgres apps do not install `mysql2`. Generated apps add `eta` because the welcome page uses it. MySQL apps also add `mysql2`. The MySQL helpers are exported from `@getstrata/core/database/mysqlConnection`, `@getstrata/core/database`, and the root barrel; importing any of them does not install `mysql2`, and apps should prefer the subpath as with every other core module.
43
43
 
44
44
  ## Views
45
45
 
@@ -10,7 +10,8 @@ export { Factory } from "./factory.ts";
10
10
  export { foreignKeyFromTable, pivotTableName, singularize } from "./inflection.ts";
11
11
  export type { CastType, GlobalScopeFn, ModelConstructor } from "./model.ts";
12
12
  export { applyCasts, BelongsToManyRelationQuery, BelongsToRelationQuery, dehydrateValue, filterMassAssignable, HasManyRelationQuery, HasManyThroughRelationQuery, HasOneRelationQuery, hydrateValue, Model, ModelQuery, MorphManyRelationQuery, MorphOneRelationQuery, MorphToRelationQuery, registerModelClass, registerModelRepository, } from "./model.ts";
13
- export { createMysqlConnection, createMysqlConnectionFromPool } from "./mysqlConnection.ts";
13
+ export type { MysqlConnection, MysqlExecutable, MysqlPool } from "./mysqlConnection.ts";
14
+ export { createMysqlConnection, createMysqlConnectionFromPool, createMysqlPool, } from "./mysqlConnection.ts";
14
15
  export type { NamedConnectionEntry } from "./namedConnections.ts";
15
16
  export { getNamedConnection, hasNamedConnection, registerNamedConnection, resetNamedConnections, runOnNamedConnection, unregisterNamedConnection, } from "./namedConnections.ts";
16
17
  export { buildAdvancedWhereClause, buildCountQuery, buildDeleteByIdQuery, buildGroupedCountQuery, buildInsertQuery, buildJoinClause, buildOrderByClause, buildProjectionQuery, buildQueryWhereClause, buildRestoreByIdQuery, buildSelectQuery, buildSoftDeleteByIdQuery, buildUpdateQuery, buildWhereClause, parseQualifiedColumn, qualifyColumn, quoteIdentifier, resolveQualifiedColumn, resolveSoftDeleteColumn, } from "./query.ts";
@@ -1,14 +1,18 @@
1
- import mysql from "mysql2/promise";
2
1
  import type { ActiveDatabaseHandle } from "./connectionContext.ts";
3
2
  type MysqlExecutable = {
4
3
  execute: (sql: string, params?: unknown[]) => Promise<[unknown, unknown]>;
5
4
  end?: () => Promise<void>;
6
5
  };
6
+ type MysqlPool = MysqlExecutable & {
7
+ end(): Promise<void>;
8
+ on(event: "connection", listener: (connection: unknown) => void): void;
9
+ };
7
10
  type MysqlConnection = ActiveDatabaseHandle & {
8
11
  close(): Promise<void>;
9
12
  };
13
+ declare function resetMysqlLoaderForTests(importer?: () => Promise<unknown>): void;
10
14
  declare function createMysqlConnectionFromPool(pool: MysqlExecutable): MysqlConnection;
11
- declare function createMysqlPool(url: string): mysql.Pool;
15
+ declare function createMysqlPool(url: string): Promise<MysqlPool>;
12
16
  declare function createMysqlConnection(url: string): MysqlConnection;
13
- export type { MysqlConnection, MysqlExecutable };
14
- export { createMysqlConnection, createMysqlConnectionFromPool, createMysqlPool };
17
+ export type { MysqlConnection, MysqlExecutable, MysqlPool };
18
+ export { createMysqlConnection, createMysqlConnectionFromPool, createMysqlPool, resetMysqlLoaderForTests, };
@@ -0,0 +1,2 @@
1
+ declare function missingOptionalPeer(packageName: string, reason: string, error: unknown): Error;
2
+ export { missingOptionalPeer };
@@ -11,9 +11,13 @@ type LayoutDataResolver = (request?: Request) => Promise<Record<string, unknown>
11
11
  * Pug class/attribute shorthand is rejected at render time.
12
12
  */
13
13
  declare class EtaViewEngine implements ViewEngine {
14
- private readonly eta;
14
+ private eta;
15
+ private etaPending;
16
+ private readonly viewsDirectory;
15
17
  private readonly resolveLayoutData?;
16
18
  constructor(viewsDirectory?: string, resolveLayoutData?: LayoutDataResolver);
19
+ private getEta;
20
+ private createEta;
17
21
  render(name: string, data?: Record<string, unknown>, options?: RenderOptions): Promise<string>;
18
22
  }
19
23
  export type { LayoutDataResolver, RenderOptions };
@@ -1,6 +1,58 @@
1
1
  // @bun
2
+ // ../../src/core/runtime/optionalPeer.ts
3
+ function missingOptionalPeer(packageName, reason, error) {
4
+ return new Error(`Install ${packageName} ${reason} (\`bun add ${packageName}\`).`, {
5
+ cause: error
6
+ });
7
+ }
8
+
2
9
  // ../../src/core/database/mysqlConnection.ts
3
- import mysql from "mysql2/promise";
10
+ var MYSQL_SESSION_UTC = "SET time_zone = '+00:00'";
11
+ var mysqlModule;
12
+ var mysqlPending;
13
+ var importMysql = defaultImportMysql;
14
+ async function defaultImportMysql() {
15
+ return import("mysql2/promise");
16
+ }
17
+ function resetMysqlLoaderForTests(importer) {
18
+ mysqlModule = undefined;
19
+ mysqlPending = undefined;
20
+ importMysql = importer ? async () => await importer() : defaultImportMysql;
21
+ }
22
+ function mysqlApi(mod) {
23
+ if (typeof mod.createPool === "function") {
24
+ return mod;
25
+ }
26
+ const withDefault = mod;
27
+ if (typeof withDefault.default?.createPool === "function") {
28
+ return withDefault.default;
29
+ }
30
+ throw new Error("mysql2/promise did not export createPool.");
31
+ }
32
+ async function loadMysql() {
33
+ if (mysqlModule) {
34
+ return mysqlModule;
35
+ }
36
+ if (!mysqlPending) {
37
+ mysqlPending = (async () => {
38
+ let mod;
39
+ try {
40
+ mod = await importMysql();
41
+ } catch (error) {
42
+ mysqlPending = undefined;
43
+ throw missingOptionalPeer("mysql2", "to open a MySQL connection", error);
44
+ }
45
+ try {
46
+ mysqlModule = mysqlApi(mod);
47
+ return mysqlModule;
48
+ } catch (error) {
49
+ mysqlPending = undefined;
50
+ throw error;
51
+ }
52
+ })();
53
+ }
54
+ return mysqlPending;
55
+ }
4
56
  function rowsFromResult(result) {
5
57
  if (Array.isArray(result)) {
6
58
  return result;
@@ -23,7 +75,6 @@ function createMysqlConnectionFromPool(pool) {
23
75
  }
24
76
  };
25
77
  }
26
- var MYSQL_SESSION_UTC = "SET time_zone = '+00:00'";
27
78
  function pinSessionToUtc(connection) {
28
79
  connection.query(MYSQL_SESSION_UTC, (error) => {
29
80
  if (error) {
@@ -31,21 +82,51 @@ function pinSessionToUtc(connection) {
31
82
  }
32
83
  });
33
84
  }
34
- function createMysqlPool(url) {
85
+ function createPoolFromModule(mysql, url) {
35
86
  const pool = mysql.createPool({ uri: url, timezone: "Z" });
36
87
  pool.on("connection", (connection) => {
37
88
  pinSessionToUtc(connection);
38
89
  });
39
90
  return pool;
40
91
  }
92
+ async function createMysqlPool(url) {
93
+ return createPoolFromModule(await loadMysql(), url);
94
+ }
41
95
  function createMysqlConnection(url) {
42
96
  if (!url.trim()) {
43
97
  throw new Error("MYSQL_URL is not configured. Set url before creating a MySQL pool.");
44
98
  }
45
- return createMysqlConnectionFromPool(createMysqlPool(url));
99
+ let poolPending;
100
+ function ensurePool() {
101
+ if (!poolPending) {
102
+ poolPending = createMysqlPool(url).catch((error) => {
103
+ poolPending = undefined;
104
+ throw error;
105
+ });
106
+ }
107
+ return poolPending;
108
+ }
109
+ return {
110
+ async unsafe(query, params = []) {
111
+ const [result] = await (await ensurePool()).execute(query, [...params]);
112
+ return rowsFromResult(result);
113
+ },
114
+ async close() {
115
+ if (!poolPending) {
116
+ return;
117
+ }
118
+ const pending = poolPending;
119
+ poolPending = undefined;
120
+ const pool = await pending.catch(() => {
121
+ return;
122
+ });
123
+ await pool?.end();
124
+ }
125
+ };
46
126
  }
47
127
  export {
48
128
  createMysqlConnection,
49
129
  createMysqlConnectionFromPool,
50
- createMysqlPool
130
+ createMysqlPool,
131
+ resetMysqlLoaderForTests
51
132
  };
@@ -2,7 +2,13 @@
2
2
  // ../../src/core/view/etaViewEngine.ts
3
3
  import { join, relative } from "path";
4
4
  import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
5
- import { Eta } from "eta";
5
+
6
+ // ../../src/core/runtime/optionalPeer.ts
7
+ function missingOptionalPeer(packageName, reason, error) {
8
+ return new Error(`Install ${packageName} ${reason} (\`bun add ${packageName}\`).`, {
9
+ cause: error
10
+ });
11
+ }
6
12
 
7
13
  // ../../src/core/view/assertEtaHtmlSource.ts
8
14
  var HTML_TAGS = new Set([
@@ -182,33 +188,57 @@ var DEFAULT_VIEWS_DIRECTORY = join(process.cwd(), "resources/views");
182
188
  var DEFAULT_LAYOUT = "layouts/app.eta";
183
189
 
184
190
  class EtaViewEngine {
185
- eta;
191
+ eta = null;
192
+ etaPending = null;
193
+ viewsDirectory;
186
194
  resolveLayoutData;
187
195
  constructor(viewsDirectory = DEFAULT_VIEWS_DIRECTORY, resolveLayoutData) {
188
- this.eta = new Eta({
189
- views: viewsDirectory,
190
- autoTrim: false
191
- });
196
+ this.viewsDirectory = viewsDirectory;
192
197
  this.resolveLayoutData = resolveLayoutData;
193
- const readFile = this.eta.readFile?.bind(this.eta);
194
- this.eta.readFile = (path) => {
198
+ }
199
+ async getEta() {
200
+ if (this.eta) {
201
+ return this.eta;
202
+ }
203
+ if (!this.etaPending) {
204
+ this.etaPending = this.createEta();
205
+ }
206
+ return this.etaPending;
207
+ }
208
+ async createEta() {
209
+ let eta;
210
+ try {
211
+ const mod = await import("eta");
212
+ eta = new mod.Eta({
213
+ views: this.viewsDirectory,
214
+ autoTrim: false
215
+ });
216
+ } catch (error) {
217
+ this.etaPending = null;
218
+ throw missingOptionalPeer("eta", "to render HTML views", error);
219
+ }
220
+ const readFile = eta.readFile?.bind(eta);
221
+ eta.readFile = (path) => {
195
222
  const source = readFile ? readFile(path) : "";
196
- assertEtaHtmlSource(relative(viewsDirectory, path) || path, source);
223
+ assertEtaHtmlSource(relative(this.viewsDirectory, path) || path, source);
197
224
  return source;
198
225
  };
226
+ this.eta = eta;
227
+ return eta;
199
228
  }
200
229
  async render(name, data = {}, options = {}) {
201
230
  const template = name.endsWith(".eta") ? name : `${name}.eta`;
202
231
  const request = options.request ?? currentRequestMeta().request;
203
232
  const layoutData = this.resolveLayoutData ? await this.resolveLayoutData(request) : {};
204
233
  const mergedData = { ...layoutData, ...data };
205
- const body = await this.eta.renderAsync(template, mergedData);
234
+ const eta = await this.getEta();
235
+ const body = await eta.renderAsync(template, mergedData);
206
236
  const layout = options.layout ?? DEFAULT_LAYOUT;
207
237
  if (layout === false) {
208
238
  return body;
209
239
  }
210
240
  const layoutTemplate = layout.endsWith(".eta") ? layout : `${layout}.eta`;
211
- return await this.eta.renderAsync(layoutTemplate, {
241
+ return await eta.renderAsync(layoutTemplate, {
212
242
  ...mergedData,
213
243
  body
214
244
  });
@@ -43,6 +43,7 @@ export { freshDatabase, getMigrationStatus, loadMigrationsFromDirectory, migrate
43
43
  export type { Migration, MigrationDatabase, MigrationStatus, } from "../core/database/migrations/types.ts";
44
44
  export type { CastType, GlobalScopeFn, ModelConstructor } from "../core/database/model.ts";
45
45
  export { applyCasts, BelongsToManyRelationQuery, BelongsToRelationQuery, dehydrateValue, filterMassAssignable, HasManyRelationQuery, HasOneRelationQuery, hydrateValue, Model, ModelQuery, MorphManyRelationQuery, MorphOneRelationQuery, MorphToRelationQuery, registerModelClass, registerModelRepository, } from "../core/database/model.ts";
46
+ export type { MysqlConnection, MysqlExecutable, MysqlPool, } from "../core/database/mysqlConnection.ts";
46
47
  export { createMysqlConnection, createMysqlConnectionFromPool, createMysqlPool, } from "../core/database/mysqlConnection.ts";
47
48
  export { getNamedConnection, hasNamedConnection, registerNamedConnection, resetNamedConnections, runOnNamedConnection, unregisterNamedConnection, } from "../core/database/namedConnections.ts";
48
49
  export { createDatabaseQueryProxy } from "../core/database/queryProxy.ts";
package/dist/index.js CHANGED
@@ -5211,8 +5211,55 @@ function registerModelRepository(model, repository) {
5211
5211
  ensureBooted(model);
5212
5212
  return model;
5213
5213
  }
5214
+ // ../../src/core/runtime/optionalPeer.ts
5215
+ function missingOptionalPeer(packageName, reason, error) {
5216
+ return new Error(`Install ${packageName} ${reason} (\`bun add ${packageName}\`).`, {
5217
+ cause: error
5218
+ });
5219
+ }
5220
+
5214
5221
  // ../../src/core/database/mysqlConnection.ts
5215
- import mysql from "mysql2/promise";
5222
+ var MYSQL_SESSION_UTC = "SET time_zone = '+00:00'";
5223
+ var mysqlModule;
5224
+ var mysqlPending;
5225
+ var importMysql = defaultImportMysql;
5226
+ async function defaultImportMysql() {
5227
+ return import("mysql2/promise");
5228
+ }
5229
+ function mysqlApi(mod) {
5230
+ if (typeof mod.createPool === "function") {
5231
+ return mod;
5232
+ }
5233
+ const withDefault = mod;
5234
+ if (typeof withDefault.default?.createPool === "function") {
5235
+ return withDefault.default;
5236
+ }
5237
+ throw new Error("mysql2/promise did not export createPool.");
5238
+ }
5239
+ async function loadMysql() {
5240
+ if (mysqlModule) {
5241
+ return mysqlModule;
5242
+ }
5243
+ if (!mysqlPending) {
5244
+ mysqlPending = (async () => {
5245
+ let mod;
5246
+ try {
5247
+ mod = await importMysql();
5248
+ } catch (error) {
5249
+ mysqlPending = undefined;
5250
+ throw missingOptionalPeer("mysql2", "to open a MySQL connection", error);
5251
+ }
5252
+ try {
5253
+ mysqlModule = mysqlApi(mod);
5254
+ return mysqlModule;
5255
+ } catch (error) {
5256
+ mysqlPending = undefined;
5257
+ throw error;
5258
+ }
5259
+ })();
5260
+ }
5261
+ return mysqlPending;
5262
+ }
5216
5263
  function rowsFromResult(result) {
5217
5264
  if (Array.isArray(result)) {
5218
5265
  return result;
@@ -5235,7 +5282,6 @@ function createMysqlConnectionFromPool(pool) {
5235
5282
  }
5236
5283
  };
5237
5284
  }
5238
- var MYSQL_SESSION_UTC = "SET time_zone = '+00:00'";
5239
5285
  function pinSessionToUtc(connection) {
5240
5286
  connection.query(MYSQL_SESSION_UTC, (error) => {
5241
5287
  if (error) {
@@ -5243,18 +5289,47 @@ function pinSessionToUtc(connection) {
5243
5289
  }
5244
5290
  });
5245
5291
  }
5246
- function createMysqlPool(url) {
5292
+ function createPoolFromModule(mysql, url) {
5247
5293
  const pool = mysql.createPool({ uri: url, timezone: "Z" });
5248
5294
  pool.on("connection", (connection) => {
5249
5295
  pinSessionToUtc(connection);
5250
5296
  });
5251
5297
  return pool;
5252
5298
  }
5299
+ async function createMysqlPool(url) {
5300
+ return createPoolFromModule(await loadMysql(), url);
5301
+ }
5253
5302
  function createMysqlConnection(url) {
5254
5303
  if (!url.trim()) {
5255
5304
  throw new Error("MYSQL_URL is not configured. Set url before creating a MySQL pool.");
5256
5305
  }
5257
- return createMysqlConnectionFromPool(createMysqlPool(url));
5306
+ let poolPending;
5307
+ function ensurePool() {
5308
+ if (!poolPending) {
5309
+ poolPending = createMysqlPool(url).catch((error) => {
5310
+ poolPending = undefined;
5311
+ throw error;
5312
+ });
5313
+ }
5314
+ return poolPending;
5315
+ }
5316
+ return {
5317
+ async unsafe(query, params = []) {
5318
+ const [result] = await (await ensurePool()).execute(query, [...params]);
5319
+ return rowsFromResult(result);
5320
+ },
5321
+ async close() {
5322
+ if (!poolPending) {
5323
+ return;
5324
+ }
5325
+ const pending = poolPending;
5326
+ poolPending = undefined;
5327
+ const pool = await pending.catch(() => {
5328
+ return;
5329
+ });
5330
+ await pool?.end();
5331
+ }
5332
+ };
5258
5333
  }
5259
5334
  // ../../src/core/database/namedConnections.ts
5260
5335
  var REGISTRY_KEY = Symbol.for("@getstrata/namedConnections");
@@ -9229,7 +9304,6 @@ function validateObject(payload, schema) {
9229
9304
  }
9230
9305
  // ../../src/core/view/etaViewEngine.ts
9231
9306
  import { join as join4, relative } from "path";
9232
- import { Eta } from "eta";
9233
9307
 
9234
9308
  // ../../src/core/view/assertEtaHtmlSource.ts
9235
9309
  var HTML_TAGS = new Set([
@@ -9409,33 +9483,57 @@ var DEFAULT_VIEWS_DIRECTORY = join4(process.cwd(), "resources/views");
9409
9483
  var DEFAULT_LAYOUT = "layouts/app.eta";
9410
9484
 
9411
9485
  class EtaViewEngine {
9412
- eta;
9486
+ eta = null;
9487
+ etaPending = null;
9488
+ viewsDirectory;
9413
9489
  resolveLayoutData;
9414
9490
  constructor(viewsDirectory = DEFAULT_VIEWS_DIRECTORY, resolveLayoutData) {
9415
- this.eta = new Eta({
9416
- views: viewsDirectory,
9417
- autoTrim: false
9418
- });
9491
+ this.viewsDirectory = viewsDirectory;
9419
9492
  this.resolveLayoutData = resolveLayoutData;
9420
- const readFile = this.eta.readFile?.bind(this.eta);
9421
- this.eta.readFile = (path) => {
9493
+ }
9494
+ async getEta() {
9495
+ if (this.eta) {
9496
+ return this.eta;
9497
+ }
9498
+ if (!this.etaPending) {
9499
+ this.etaPending = this.createEta();
9500
+ }
9501
+ return this.etaPending;
9502
+ }
9503
+ async createEta() {
9504
+ let eta;
9505
+ try {
9506
+ const mod = await import("eta");
9507
+ eta = new mod.Eta({
9508
+ views: this.viewsDirectory,
9509
+ autoTrim: false
9510
+ });
9511
+ } catch (error) {
9512
+ this.etaPending = null;
9513
+ throw missingOptionalPeer("eta", "to render HTML views", error);
9514
+ }
9515
+ const readFile = eta.readFile?.bind(eta);
9516
+ eta.readFile = (path) => {
9422
9517
  const source = readFile ? readFile(path) : "";
9423
- assertEtaHtmlSource(relative(viewsDirectory, path) || path, source);
9518
+ assertEtaHtmlSource(relative(this.viewsDirectory, path) || path, source);
9424
9519
  return source;
9425
9520
  };
9521
+ this.eta = eta;
9522
+ return eta;
9426
9523
  }
9427
9524
  async render(name, data = {}, options = {}) {
9428
9525
  const template = name.endsWith(".eta") ? name : `${name}.eta`;
9429
9526
  const request = options.request ?? currentRequestMeta().request;
9430
9527
  const layoutData = this.resolveLayoutData ? await this.resolveLayoutData(request) : {};
9431
9528
  const mergedData = { ...layoutData, ...data };
9432
- const body = await this.eta.renderAsync(template, mergedData);
9529
+ const eta = await this.getEta();
9530
+ const body = await eta.renderAsync(template, mergedData);
9433
9531
  const layout = options.layout ?? DEFAULT_LAYOUT;
9434
9532
  if (layout === false) {
9435
9533
  return body;
9436
9534
  }
9437
9535
  const layoutTemplate = layout.endsWith(".eta") ? layout : `${layout}.eta`;
9438
- return await this.eta.renderAsync(layoutTemplate, {
9536
+ return await eta.renderAsync(layoutTemplate, {
9439
9537
  ...mergedData,
9440
9538
  body
9441
9539
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/core",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Strata Bun framework public API",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -904,13 +904,19 @@
904
904
  "publishConfig": {
905
905
  "access": "public"
906
906
  },
907
- "dependencies": {
908
- "eta": "^4.6.0",
909
- "mysql2": "^3.24.3"
910
- },
911
907
  "peerDependencies": {
908
+ "eta": "^4.6.0",
909
+ "mysql2": "^3.24.3",
912
910
  "typescript": "^5.9.0"
913
911
  },
912
+ "peerDependenciesMeta": {
913
+ "eta": {
914
+ "optional": true
915
+ },
916
+ "mysql2": {
917
+ "optional": true
918
+ }
919
+ },
914
920
  "engines": {
915
921
  "bun": ">=1.4.0"
916
922
  }