@remix-run/data-table-mysql 0.4.0 → 0.5.1

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.
@@ -0,0 +1,46 @@
1
+ import { Database, type DatabaseOptions } from '@remix-run/data-table'
2
+
3
+ import { MysqlDatabaseDriver, type MysqlDatabaseInput } from './driver.ts'
4
+
5
+ /** Options for creating a MySQL database. */
6
+ export interface MysqlDatabaseOptions extends DatabaseOptions {
7
+ /** Character set assigned to the recreated database. */
8
+ characterSet?: string
9
+ /** Collation assigned to the recreated database. */
10
+ collation?: string
11
+ }
12
+
13
+ /** A {@link Database} backed by MySQL. */
14
+ export class MysqlDatabase extends Database<'mysql'> {
15
+ /**
16
+ * Creates a MySQL-backed database.
17
+ * @param input MySQL pool configuration, pool, connection, or URI.
18
+ * @param options Database runtime and recreation options.
19
+ */
20
+ constructor(input: MysqlDatabaseInput, options: MysqlDatabaseOptions = {}) {
21
+ super(new MysqlDatabaseDriver(input, options), options)
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Creates a MySQL-backed database.
27
+ *
28
+ * @param input MySQL pool configuration, pool, connection, or URI.
29
+ * @param options Database runtime and recreation options.
30
+ * @returns A MySQL database.
31
+ * @example
32
+ * ```ts
33
+ * import { createMysqlDatabase } from 'remix/data-table/mysql'
34
+ *
35
+ * let db = createMysqlDatabase({
36
+ * uri: process.env.DATABASE_URL,
37
+ * multipleStatements: true,
38
+ * })
39
+ * ```
40
+ */
41
+ export function createMysqlDatabase(
42
+ input: MysqlDatabaseInput,
43
+ options: MysqlDatabaseOptions = {},
44
+ ): MysqlDatabase {
45
+ return new MysqlDatabase(input, options)
46
+ }
@@ -1,18 +1,22 @@
1
1
  import type {
2
+ DataManipulationOperation,
2
3
  DataManipulationRequest,
3
4
  DataManipulationResult,
4
- DataManipulationOperation,
5
- DatabaseAdapter,
5
+ DatabaseDriver,
6
6
  SqlStatement,
7
7
  TableRef,
8
8
  TransactionOptions,
9
9
  TransactionToken,
10
10
  } from '@remix-run/data-table'
11
+ import { AsyncLocalStorage } from 'node:async_hooks'
12
+
11
13
  import { getTablePrimaryKey } from '@remix-run/data-table'
14
+ import mysql from 'mysql2/promise'
12
15
  import type {
13
16
  Connection as MysqlConnection,
14
17
  Pool as MysqlPool,
15
18
  PoolConnection as MysqlPoolConnection,
19
+ PoolOptions as MysqlPoolOptions,
16
20
  ResultSetHeader,
17
21
  RowDataPacket,
18
22
  } from 'mysql2/promise'
@@ -32,33 +36,62 @@ type MysqlQueryResultHeader = {
32
36
  type MysqlTransactionConnection = MysqlConnection | MysqlPoolConnection
33
37
  type MysqlQueryable = MysqlPool | MysqlTransactionConnection
34
38
 
39
+ export type MysqlDatabaseInput = string | MysqlPoolOptions | MysqlQueryable
40
+
41
+ const mysqlCapabilities = Object.freeze({
42
+ returning: false,
43
+ savepoints: true,
44
+ upsert: true,
45
+ transactionalDdl: false,
46
+ migrationLock: true,
47
+ })
48
+
49
+ /** Database creation options used when wiping a config-backed MySQL driver. */
50
+ export interface MysqlDatabaseDriverOptions {
51
+ /** Character set assigned to the recreated database. */
52
+ characterSet?: string
53
+ /** Collation assigned to the recreated database. */
54
+ collation?: string
55
+ }
56
+
35
57
  /**
36
- * `DatabaseAdapter` implementation for mysql-compatible clients.
58
+ * MySQL database driver backed by a mysql-compatible client.
37
59
  */
38
- export class MysqlDatabaseAdapter implements DatabaseAdapter {
60
+ export class MysqlDatabaseDriver implements DatabaseDriver<'mysql'> {
39
61
  /**
40
- * The SQL dialect identifier reported by this adapter.
62
+ * The SQL dialect identifier reported by this database.
41
63
  */
42
- dialect = 'mysql'
64
+ get dialect(): 'mysql' {
65
+ return 'mysql'
66
+ }
43
67
 
44
68
  /**
45
- * Feature flags describing the mysql behaviors supported by this adapter.
69
+ * Feature flags describing the MySQL behaviors supported by this database.
46
70
  */
47
- capabilities
71
+ get capabilities() {
72
+ return mysqlCapabilities
73
+ }
48
74
 
75
+ #config?: string | MysqlPoolOptions
49
76
  #client: MysqlQueryable
77
+ #characterSet?: string
78
+ #collation?: string
50
79
  #transactions = new Map<string, TransactionState>()
51
80
  #transactionCounter = 0
81
+ #migrationLockQueue = Promise.resolve()
82
+ #migrationLockStore = new AsyncLocalStorage<boolean>()
83
+ #poolClosed = false
52
84
 
53
- constructor(client: MysqlQueryable) {
54
- this.#client = client
55
- this.capabilities = {
56
- returning: false,
57
- savepoints: true,
58
- upsert: true,
59
- transactionalDdl: false,
60
- migrationLock: true,
85
+ constructor(config: MysqlDatabaseInput, options: MysqlDatabaseDriverOptions = {}) {
86
+ if (isMysqlQueryable(config)) {
87
+ this.#client = config
88
+ } else {
89
+ this.#config = config
90
+ this.#client = createMysqlPool(config)
61
91
  }
92
+
93
+ this.#characterSet = options.characterSet
94
+ this.#collation = options.collation
62
95
  }
63
96
 
64
97
  /**
@@ -187,17 +220,24 @@ export class MysqlDatabaseAdapter implements DatabaseAdapter {
187
220
  connection = this.#client
188
221
  }
189
222
 
190
- if (options?.isolationLevel) {
191
- await connection.query('set transaction isolation level ' + options.isolationLevel)
192
- }
223
+ try {
224
+ if (options?.isolationLevel) {
225
+ await connection.query('set transaction isolation level ' + options.isolationLevel)
226
+ }
193
227
 
194
- if (options?.readOnly !== undefined) {
195
- await connection.query(
196
- options.readOnly ? 'set transaction read only' : 'set transaction read write',
197
- )
198
- }
228
+ if (options?.readOnly !== undefined) {
229
+ await connection.query(
230
+ options.readOnly ? 'set transaction read only' : 'set transaction read write',
231
+ )
232
+ }
199
233
 
200
- await connection.beginTransaction()
234
+ await connection.beginTransaction()
235
+ } catch (error) {
236
+ if (releaseOnClose) {
237
+ destroyMysqlConnection(connection)
238
+ }
239
+ throw error
240
+ }
201
241
 
202
242
  this.#transactionCounter += 1
203
243
  let token = { id: 'tx_' + String(this.#transactionCounter) }
@@ -222,13 +262,21 @@ export class MysqlDatabaseAdapter implements DatabaseAdapter {
222
262
  throw new Error('Unknown transaction token: ' + token.id)
223
263
  }
224
264
 
265
+ let failed = false
225
266
  try {
226
267
  await transaction.connection.commit()
268
+ } catch (error) {
269
+ failed = true
270
+ throw error
227
271
  } finally {
228
272
  this.#transactions.delete(token.id)
229
273
 
230
- if (transaction.releaseOnClose && isMysqlPoolConnection(transaction.connection)) {
231
- transaction.connection.release()
274
+ if (transaction.releaseOnClose) {
275
+ if (failed) {
276
+ destroyMysqlConnection(transaction.connection)
277
+ } else if (isMysqlPoolConnection(transaction.connection)) {
278
+ transaction.connection.release()
279
+ }
232
280
  }
233
281
  }
234
282
  }
@@ -245,13 +293,21 @@ export class MysqlDatabaseAdapter implements DatabaseAdapter {
245
293
  throw new Error('Unknown transaction token: ' + token.id)
246
294
  }
247
295
 
296
+ let failed = false
248
297
  try {
249
298
  await transaction.connection.rollback()
299
+ } catch (error) {
300
+ failed = true
301
+ throw error
250
302
  } finally {
251
303
  this.#transactions.delete(token.id)
252
304
 
253
- if (transaction.releaseOnClose && isMysqlPoolConnection(transaction.connection)) {
254
- transaction.connection.release()
305
+ if (transaction.releaseOnClose) {
306
+ if (failed) {
307
+ destroyMysqlConnection(transaction.connection)
308
+ } else if (isMysqlPoolConnection(transaction.connection)) {
309
+ transaction.connection.release()
310
+ }
255
311
  }
256
312
  }
257
313
  }
@@ -290,19 +346,144 @@ export class MysqlDatabaseAdapter implements DatabaseAdapter {
290
346
  }
291
347
 
292
348
  /**
293
- * Acquires the mysql migration lock.
294
- * @returns A promise that resolves when the lock is acquired.
349
+ * Destructively recreates the configured MySQL database.
350
+ * @returns A promise that resolves when the database is ready for use.
295
351
  */
296
- async acquireMigrationLock(): Promise<void> {
297
- await this.#client.query('select get_lock(?, 60)', ['data_table_migrations'])
352
+ async wipe(): Promise<void> {
353
+ let config = this.#configOrThrow('wipe')
354
+ this.#assertNoOpenTransactions('wipe')
355
+ let database = resolveMysqlDatabaseName(config)
356
+ await this.#closePool()
357
+ let connection: MysqlConnection | undefined
358
+
359
+ try {
360
+ connection = await createMysqlConnection(toMysqlServerConfig(config))
361
+ await connection.query('drop database if exists ' + quoteIdentifier(database))
362
+
363
+ let sql = 'create database ' + quoteIdentifier(database)
364
+
365
+ if (this.#characterSet) {
366
+ sql += ' character set ' + quoteIdentifier(this.#characterSet)
367
+ }
368
+
369
+ if (this.#collation) {
370
+ sql += ' collate ' + quoteIdentifier(this.#collation)
371
+ }
372
+
373
+ await connection.query(sql)
374
+ } finally {
375
+ try {
376
+ await connection?.end()
377
+ } finally {
378
+ await this.#replacePool()
379
+ }
380
+ }
381
+ }
382
+
383
+ /** Closes a pool created from configuration. Supplied connections and pools remain caller-owned. */
384
+ async close(): Promise<void> {
385
+ this.#assertNoOpenTransactions('close')
386
+ if (this.#config) {
387
+ await this.#closePool()
388
+ }
298
389
  }
299
390
 
300
391
  /**
301
- * Releases the mysql migration lock.
302
- * @returns A promise that resolves when the lock is released.
392
+ * Runs migration work on the mysql connection that owns the named lock.
393
+ *
394
+ * Lock acquisition waits up to 60 seconds and throws when the lock cannot
395
+ * be acquired. Re-entering this method from inside `run` throws instead of
396
+ * deadlocking, and a failed run destroys the reserved connection instead of
397
+ * returning it to the pool.
398
+ * @param name Logical migration lock name.
399
+ * @param run Migration work to run with a connection-bound driver.
400
+ * @returns The callback result.
303
401
  */
304
- async releaseMigrationLock(): Promise<void> {
305
- await this.#client.query('select release_lock(?)', ['data_table_migrations'])
402
+ async withMigrationLock<result>(
403
+ name: string,
404
+ run: (driver: DatabaseDriver<'mysql'>) => Promise<result>,
405
+ ): Promise<result> {
406
+ if (this.#migrationLockStore.getStore()) {
407
+ throw new Error('MySQL migration lock is already held by this database')
408
+ }
409
+
410
+ let waitForPreviousLock = this.#migrationLockQueue
411
+ let releaseQueue: () => void = () => undefined
412
+ this.#migrationLockQueue = new Promise((resolve) => {
413
+ releaseQueue = resolve
414
+ })
415
+
416
+ await waitForPreviousLock
417
+
418
+ try {
419
+ let releaseOnClose = false
420
+ let connection: MysqlTransactionConnection
421
+
422
+ if (isMysqlPool(this.#client)) {
423
+ connection = await this.#client.getConnection()
424
+ releaseOnClose = true
425
+ } else {
426
+ connection = this.#client
427
+ }
428
+
429
+ let driver = releaseOnClose ? new MysqlDatabaseDriver(connection) : this
430
+
431
+ try {
432
+ let value = await this.#migrationLockStore.run(true, () =>
433
+ runWithMysqlMigrationLock(connection, name, driver, run),
434
+ )
435
+
436
+ if (releaseOnClose && isMysqlPoolConnection(connection)) {
437
+ connection.release()
438
+ }
439
+
440
+ return value
441
+ } catch (error) {
442
+ // A failed run can leave the reserved connection dirty (open
443
+ // transaction, still-held named lock), so destroy the connection
444
+ // instead of returning it to the pool.
445
+ if (releaseOnClose) {
446
+ destroyMysqlConnection(connection)
447
+ }
448
+
449
+ throw error
450
+ }
451
+ } finally {
452
+ releaseQueue()
453
+ }
454
+ }
455
+
456
+ async #closePool(): Promise<void> {
457
+ this.#transactions.clear()
458
+ // mysql2 pools error on end() when called twice, so ending must be
459
+ // tracked to keep close() idempotent.
460
+ if (isMysqlPool(this.#client) && !this.#poolClosed) {
461
+ this.#poolClosed = true
462
+ await this.#client.end()
463
+ }
464
+ }
465
+
466
+ async #replacePool(): Promise<void> {
467
+ await this.#closePool().catch(() => undefined)
468
+
469
+ if (this.#config) {
470
+ this.#client = createMysqlPool(this.#config)
471
+ this.#poolClosed = false
472
+ }
473
+ }
474
+
475
+ #configOrThrow(method: string): string | MysqlPoolOptions {
476
+ if (!this.#config) {
477
+ throw new Error('MySQL database ' + method + '() requires config-based construction')
478
+ }
479
+
480
+ return this.#config
481
+ }
482
+
483
+ #assertNoOpenTransactions(method: string): void {
484
+ if (this.#transactions.size > 0) {
485
+ throw new Error('MySQL database cannot ' + method + ' while transactions are open')
486
+ }
306
487
  }
307
488
 
308
489
  #resolveClient(token: TransactionToken | undefined): MysqlQueryable {
@@ -324,36 +505,159 @@ export class MysqlDatabaseAdapter implements DatabaseAdapter {
324
505
  }
325
506
  }
326
507
 
327
- /**
328
- * Creates a mysql `DatabaseAdapter`.
329
- * @param client Mysql pool or connection.
330
- * @param options Optional adapter capability overrides.
331
- * @returns A configured mysql adapter.
332
- * @example
333
- * ```ts
334
- * import { createPool } from 'mysql2/promise'
335
- * import { createDatabase } from 'remix/data-table'
336
- * import { createMysqlDatabaseAdapter } from 'remix/data-table/mysql'
337
- *
338
- * let pool = createPool({ uri: process.env.DATABASE_URL })
339
- * let adapter = createMysqlDatabaseAdapter(pool)
340
- * let db = createDatabase(adapter)
341
- * ```
342
- */
343
- export function createMysqlDatabaseAdapter(client: MysqlQueryable): MysqlDatabaseAdapter {
344
- return new MysqlDatabaseAdapter(client)
508
+ function isMysqlQueryable(value: unknown): value is MysqlQueryable {
509
+ return typeof value === 'object' && value !== null && 'query' in value
510
+ }
511
+
512
+ function createMysqlPool(config: string | MysqlPoolOptions): MysqlPool {
513
+ return typeof config === 'string' ? mysql.createPool(config) : mysql.createPool(config)
514
+ }
515
+
516
+ function createMysqlConnection(config: string | MysqlPoolOptions): Promise<MysqlConnection> {
517
+ return typeof config === 'string'
518
+ ? mysql.createConnection(config)
519
+ : mysql.createConnection(config)
345
520
  }
346
521
 
347
522
  function isMysqlPool(client: MysqlQueryable): client is MysqlPool {
348
523
  return 'getConnection' in client && typeof client.getConnection === 'function'
349
524
  }
350
525
 
526
+ function resolveMysqlDatabaseName(config: string | MysqlPoolOptions): string {
527
+ let database =
528
+ typeof config === 'string'
529
+ ? resolveDatabaseNameFromUrl(config)
530
+ : (config.database ?? resolveDatabaseNameFromUrl(config.uri ?? ''))
531
+
532
+ if (!database) {
533
+ throw new Error('MySQL database config requires a database name')
534
+ }
535
+
536
+ return database
537
+ }
538
+
539
+ function resolveDatabaseNameFromUrl(value: string): string | undefined {
540
+ try {
541
+ let url = new URL(value)
542
+ let database = decodeURIComponent(url.pathname.replace(/^\//, ''))
543
+ return database || undefined
544
+ } catch {
545
+ return undefined
546
+ }
547
+ }
548
+
549
+ function toMysqlServerConfig(config: string | MysqlPoolOptions): string | MysqlPoolOptions {
550
+ if (typeof config === 'string') {
551
+ try {
552
+ let url = new URL(config)
553
+ url.pathname = '/'
554
+ return url.toString()
555
+ } catch {
556
+ return config
557
+ }
558
+ }
559
+
560
+ // Pool-only options make mysql2's createConnection() log "Ignoring invalid
561
+ // configuration option" warnings, so strip them from the maintenance
562
+ // connection config.
563
+ let {
564
+ database: _database,
565
+ uri,
566
+ connectionLimit: _connectionLimit,
567
+ maxIdle: _maxIdle,
568
+ idleTimeout: _idleTimeout,
569
+ queueLimit: _queueLimit,
570
+ waitForConnections: _waitForConnections,
571
+ ...serverConfig
572
+ } = config
573
+
574
+ if (uri === undefined) {
575
+ return serverConfig
576
+ }
577
+
578
+ return { ...serverConfig, uri: removeDatabaseFromUrl(uri) }
579
+ }
580
+
581
+ function removeDatabaseFromUrl(value: string): string {
582
+ try {
583
+ let url = new URL(value)
584
+ url.pathname = '/'
585
+ return url.toString()
586
+ } catch {
587
+ return value
588
+ }
589
+ }
590
+
351
591
  function isMysqlPoolConnection(
352
592
  connection: MysqlTransactionConnection,
353
593
  ): connection is MysqlPoolConnection {
354
594
  return 'release' in connection && typeof connection.release === 'function'
355
595
  }
356
596
 
597
+ function destroyMysqlConnection(connection: MysqlTransactionConnection): void {
598
+ let destroy = (connection as { destroy?: () => void }).destroy
599
+
600
+ if (typeof destroy === 'function') {
601
+ destroy.call(connection)
602
+ return
603
+ }
604
+
605
+ void connection.end().catch(() => undefined)
606
+ }
607
+
608
+ async function runWithMysqlMigrationLock<result>(
609
+ connection: MysqlTransactionConnection,
610
+ name: string,
611
+ driver: MysqlDatabaseDriver,
612
+ run: (driver: DatabaseDriver<'mysql'>) => Promise<result>,
613
+ ): Promise<result> {
614
+ // sha2(..., 256) yields 64 hex characters, exactly GET_LOCK's 64-character
615
+ // lock name limit, so any additional prefix must go inside the hash input.
616
+ let [lockRows] = await connection.query(
617
+ "select lock_name, get_lock(lock_name, 60) as `acquired` from (select sha2(concat(coalesce(database(), ''), ':', ?), 256) as lock_name) as migration_lock",
618
+ [name],
619
+ )
620
+
621
+ let lockRow = isRowsResult(lockRows) ? lockRows[0] : undefined
622
+ let lockName = lockRow?.lock_name
623
+
624
+ if (typeof lockName !== 'string' || !toBooleanExists(lockRow?.acquired)) {
625
+ throw new Error('MySQL migration lock could not be acquired')
626
+ }
627
+
628
+ let outcome: { status: 'success'; value: result } | { status: 'failure'; error: unknown }
629
+
630
+ try {
631
+ outcome = { status: 'success', value: await run(driver) }
632
+ } catch (error) {
633
+ outcome = { status: 'failure', error }
634
+ }
635
+
636
+ let unlockFailed = false
637
+ let unlockError: unknown
638
+
639
+ try {
640
+ let [unlockRows] = await connection.query('select release_lock(?) as `released`', [lockName])
641
+
642
+ if (!isRowsResult(unlockRows) || !toBooleanExists(unlockRows[0]?.released)) {
643
+ throw new Error('MySQL migration lock was not held by the reserved connection')
644
+ }
645
+ } catch (error) {
646
+ unlockFailed = true
647
+ unlockError = error
648
+ }
649
+
650
+ if (outcome.status === 'failure') {
651
+ throw outcome.error
652
+ }
653
+
654
+ if (unlockFailed) {
655
+ throw unlockError
656
+ }
657
+
658
+ return outcome.value
659
+ }
660
+
357
661
  function isRowsResult(result: unknown): result is MysqlQueryRows {
358
662
  return Array.isArray(result) && (result.length === 0 || !Array.isArray(result[0]))
359
663
  }
@@ -1,5 +1,6 @@
1
1
  import { getTableName, getTablePrimaryKey } from '@remix-run/data-table'
2
- import type { DataManipulationOperation, Predicate, SqlStatement } from '@remix-run/data-table'
2
+ import type { Predicate, SqlStatement } from '@remix-run/data-table'
3
+ import type { DataManipulationOperation } from '@remix-run/data-table'
3
4
  import {
4
5
  collectColumns as collectColumnsHelper,
5
6
  normalizeJoinType as normalizeJoinTypeHelper,
@@ -43,8 +44,8 @@ export function compileMysqlOperation(operation: DataManipulationOperation): Sql
43
44
  compileGroupByClause(operation.groupBy) +
44
45
  compileHavingClause(operation.having, context) +
45
46
  compileOrderByClause(operation.orderBy) +
46
- compileLimitClause(operation.limit) +
47
- compileOffsetClause(operation.offset),
47
+ compileLimitClause(operation.limit, context) +
48
+ compileOffsetClause(operation.offset, context),
48
49
  values: context.values,
49
50
  }
50
51
  }
@@ -279,20 +280,20 @@ function compileOrderByClause(orderBy: { column: string; direction: 'asc' | 'des
279
280
  )
280
281
  }
281
282
 
282
- function compileLimitClause(limit: number | undefined): string {
283
+ function compileLimitClause(limit: number | undefined, context: CompileContext): string {
283
284
  if (limit === undefined) {
284
285
  return ''
285
286
  }
286
287
 
287
- return ' limit ' + String(limit)
288
+ return ' limit ' + pushValue(context, limit)
288
289
  }
289
290
 
290
- function compileOffsetClause(offset: number | undefined): string {
291
+ function compileOffsetClause(offset: number | undefined, context: CompileContext): string {
291
292
  if (offset === undefined) {
292
293
  return ''
293
294
  }
294
295
 
295
- return ' offset ' + String(offset)
296
+ return ' offset ' + pushValue(context, offset)
296
297
  }
297
298
 
298
299
  function compilePredicate(predicate: Predicate, context: CompileContext): string {
@@ -1 +0,0 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/lib/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,uBAAuB,EACvB,sBAAsB,EACtB,yBAAyB,EACzB,eAAe,EACf,YAAY,EACZ,QAAQ,EACR,kBAAkB,EAClB,gBAAgB,EACjB,MAAM,uBAAuB,CAAA;AAE9B,OAAO,KAAK,EACV,UAAU,IAAI,eAAe,EAC7B,IAAI,IAAI,SAAS,EACjB,cAAc,IAAI,mBAAmB,EAGtC,MAAM,gBAAgB,CAAA;AAcvB,KAAK,0BAA0B,GAAG,eAAe,GAAG,mBAAmB,CAAA;AACvE,KAAK,cAAc,GAAG,SAAS,GAAG,0BAA0B,CAAA;AAE5D;;GAEG;AACH,qBAAa,oBAAqB,YAAW,eAAe;;IAC1D;;OAEG;IACH,OAAO,SAAU;IAEjB;;OAEG;IACH,YAAY;;;;;;MAAA;IAMZ,YAAY,MAAM,EAAE,cAAc,EASjC;IAED;;;;OAIG;IACH,UAAU,CAAC,SAAS,EAAE,yBAAyB,GAAG,YAAY,EAAE,CAG/D;IAED;;;;OAIG;IACG,OAAO,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CA8B/E;IAED;;;;;;;;OAQG;IACG,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAG9E;IAED;;;;;OAKG;IACG,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,WAAW,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CAchF;IAED;;;;;;OAMG;IACG,SAAS,CACb,KAAK,EAAE,QAAQ,EACf,MAAM,EAAE,MAAM,EACd,WAAW,CAAC,EAAE,gBAAgB,GAC7B,OAAO,CAAC,OAAO,CAAC,CAclB;IAED;;;;OAIG;IACG,gBAAgB,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAgC9E;IAED;;;;OAIG;IACG,iBAAiB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB9D;IAED;;;;OAIG;IACG,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBhE;IAED;;;;;OAKG;IACG,eAAe,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG1E;IAED;;;;;OAKG;IACG,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG9E;IAED;;;;;OAKG;IACG,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG3E;IAED;;;OAGG;IACG,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAE1C;IAED;;;OAGG;IACG,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAE1C;CAmBF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,cAAc,GAAG,oBAAoB,CAEvF"}