@enbox/dwn-sql-store 0.0.2 → 0.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.
Files changed (85) hide show
  1. package/README.md +36 -53
  2. package/dist/esm/src/data-store-sql.js +5 -5
  3. package/dist/esm/src/data-store-sql.js.map +1 -1
  4. package/dist/esm/src/dialect/bun-sqlite-adapter.js +46 -0
  5. package/dist/esm/src/dialect/bun-sqlite-adapter.js.map +1 -0
  6. package/dist/esm/src/dialect/mysql-dialect.js +1 -1
  7. package/dist/esm/src/dialect/mysql-dialect.js.map +1 -1
  8. package/dist/esm/src/dialect/postgres-dialect.js +1 -1
  9. package/dist/esm/src/dialect/postgres-dialect.js.map +1 -1
  10. package/dist/esm/src/dialect/sqlite-dialect.js +1 -1
  11. package/dist/esm/src/dialect/sqlite-dialect.js.map +1 -1
  12. package/dist/esm/src/main.js +3 -1
  13. package/dist/esm/src/main.js.map +1 -1
  14. package/dist/esm/src/message-store-sql.js +54 -25
  15. package/dist/esm/src/message-store-sql.js.map +1 -1
  16. package/dist/esm/src/resumable-task-store-sql.js +5 -6
  17. package/dist/esm/src/resumable-task-store-sql.js.map +1 -1
  18. package/dist/esm/src/smt-store-sql.js +151 -0
  19. package/dist/esm/src/smt-store-sql.js.map +1 -0
  20. package/dist/esm/src/state-index-sql.js +234 -0
  21. package/dist/esm/src/state-index-sql.js.map +1 -0
  22. package/dist/esm/src/utils/filter.js +3 -3
  23. package/dist/esm/src/utils/filter.js.map +1 -1
  24. package/dist/esm/src/utils/sanitize.js +7 -8
  25. package/dist/esm/src/utils/sanitize.js.map +1 -1
  26. package/dist/esm/src/utils/tags.js +3 -6
  27. package/dist/esm/src/utils/tags.js.map +1 -1
  28. package/dist/esm/src/utils/transaction.js +3 -21
  29. package/dist/esm/src/utils/transaction.js.map +1 -1
  30. package/dist/types/src/data-store-sql.d.ts +3 -4
  31. package/dist/types/src/data-store-sql.d.ts.map +1 -1
  32. package/dist/types/src/dialect/bun-sqlite-adapter.d.ts +33 -0
  33. package/dist/types/src/dialect/bun-sqlite-adapter.d.ts.map +1 -0
  34. package/dist/types/src/dialect/dialect.d.ts +1 -2
  35. package/dist/types/src/dialect/dialect.d.ts.map +1 -1
  36. package/dist/types/src/dialect/mysql-dialect.d.ts +3 -2
  37. package/dist/types/src/dialect/mysql-dialect.d.ts.map +1 -1
  38. package/dist/types/src/dialect/postgres-dialect.d.ts +3 -2
  39. package/dist/types/src/dialect/postgres-dialect.d.ts.map +1 -1
  40. package/dist/types/src/dialect/sqlite-dialect.d.ts +3 -2
  41. package/dist/types/src/dialect/sqlite-dialect.d.ts.map +1 -1
  42. package/dist/types/src/main.d.ts +3 -1
  43. package/dist/types/src/main.d.ts.map +1 -1
  44. package/dist/types/src/message-store-sql.d.ts +4 -3
  45. package/dist/types/src/message-store-sql.d.ts.map +1 -1
  46. package/dist/types/src/resumable-task-store-sql.d.ts +2 -2
  47. package/dist/types/src/resumable-task-store-sql.d.ts.map +1 -1
  48. package/dist/types/src/smt-store-sql.d.ts +37 -0
  49. package/dist/types/src/smt-store-sql.d.ts.map +1 -0
  50. package/dist/types/src/state-index-sql.d.ts +44 -0
  51. package/dist/types/src/state-index-sql.d.ts.map +1 -0
  52. package/dist/types/src/types.d.ts +24 -42
  53. package/dist/types/src/types.d.ts.map +1 -1
  54. package/dist/types/src/utils/filter.d.ts +3 -3
  55. package/dist/types/src/utils/filter.d.ts.map +1 -1
  56. package/dist/types/src/utils/sanitize.d.ts +2 -2
  57. package/dist/types/src/utils/sanitize.d.ts.map +1 -1
  58. package/dist/types/src/utils/tags.d.ts +3 -5
  59. package/dist/types/src/utils/tags.d.ts.map +1 -1
  60. package/dist/types/src/utils/transaction.d.ts +4 -4
  61. package/dist/types/src/utils/transaction.d.ts.map +1 -1
  62. package/package.json +24 -36
  63. package/src/data-store-sql.ts +11 -9
  64. package/src/dialect/bun-sqlite-adapter.ts +82 -0
  65. package/src/dialect/dialect.ts +4 -5
  66. package/src/dialect/mysql-dialect.ts +8 -6
  67. package/src/dialect/postgres-dialect.ts +11 -6
  68. package/src/dialect/sqlite-dialect.ts +11 -6
  69. package/src/main.ts +4 -2
  70. package/src/message-store-sql.ts +90 -45
  71. package/src/resumable-task-store-sql.ts +9 -7
  72. package/src/smt-store-sql.ts +206 -0
  73. package/src/state-index-sql.ts +283 -0
  74. package/src/types.ts +32 -47
  75. package/src/utils/filter.ts +8 -6
  76. package/src/utils/sanitize.ts +19 -20
  77. package/src/utils/tags.ts +6 -7
  78. package/src/utils/transaction.ts +7 -23
  79. package/dist/cjs/main.js +0 -3784
  80. package/dist/cjs/package.json +0 -1
  81. package/dist/esm/src/event-log-sql.js +0 -169
  82. package/dist/esm/src/event-log-sql.js.map +0 -1
  83. package/dist/types/src/event-log-sql.d.ts +0 -24
  84. package/dist/types/src/event-log-sql.d.ts.map +0 -1
  85. package/src/event-log-sql.ts +0 -227
@@ -1,8 +1,9 @@
1
- import { DataStore, DataStream, DataStoreGetResult, DataStorePutResult } from '@enbox/dwn-sdk-js';
1
+ import type { Dialect } from './dialect/dialect.js';
2
+ import type { DwnDatabaseType } from './types.js';
3
+ import type { DataStore, DataStoreGetResult, DataStorePutResult } from '@enbox/dwn-sdk-js';
4
+
5
+ import { DataStream } from '@enbox/dwn-sdk-js';
2
6
  import { Kysely } from 'kysely';
3
- import { Readable } from 'readable-stream';
4
- import { DwnDatabaseType } from './types.js';
5
- import { Dialect } from './dialect/dialect.js';
6
7
 
7
8
  export class DataStoreSql implements DataStore {
8
9
  #dialect: Dialect;
@@ -78,12 +79,13 @@ export class DataStoreSql implements DataStore {
78
79
  return undefined;
79
80
  }
80
81
 
82
+ const dataBytes = new Uint8Array(result.data);
81
83
  return {
82
84
  dataSize : result.data.length,
83
- dataStream : new Readable({
84
- read() {
85
- this.push(Buffer.from(result.data));
86
- this.push(null);
85
+ dataStream : new ReadableStream<Uint8Array>({
86
+ start(controller): void {
87
+ controller.enqueue(dataBytes);
88
+ controller.close();
87
89
  }
88
90
  }),
89
91
  };
@@ -93,7 +95,7 @@ export class DataStoreSql implements DataStore {
93
95
  tenant: string,
94
96
  recordId: string,
95
97
  dataCid: string,
96
- dataStream: Readable
98
+ dataStream: ReadableStream<Uint8Array>
97
99
  ): Promise<DataStorePutResult> {
98
100
  if (!this.#db) {
99
101
  throw new Error(
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Adapter that wraps Bun's built-in SQLite (`bun:sqlite`) to conform to Kysely's
3
+ * `SqliteDatabase` / `SqliteStatement` interfaces, enabling it as a drop-in
4
+ * replacement for `better-sqlite3`.
5
+ */
6
+ import { Database as BunDatabase } from 'bun:sqlite';
7
+
8
+ /**
9
+ * Matches Kysely's SqliteStatement interface.
10
+ */
11
+ interface KyselySqliteStatement {
12
+ readonly reader: boolean;
13
+ all(parameters: ReadonlyArray<unknown>): unknown[];
14
+ run(parameters: ReadonlyArray<unknown>): {
15
+ changes: number | bigint;
16
+ lastInsertRowid: number | bigint;
17
+ };
18
+ }
19
+
20
+ /**
21
+ * Matches Kysely's SqliteDatabase interface.
22
+ */
23
+ interface KyselySqliteDatabase {
24
+ close(): void;
25
+ prepare(sql: string): KyselySqliteStatement;
26
+ }
27
+
28
+ /** SQL command prefixes that indicate a read/query operation. */
29
+ const READER_PREFIXES = /^\s*(SELECT|PRAGMA|EXPLAIN|WITH)\b/i;
30
+
31
+ /**
32
+ * Detects `RETURNING` clause in DML statements (INSERT/UPDATE/DELETE ... RETURNING).
33
+ * These produce rows like a SELECT, so the statement must use `all()` not `run()`.
34
+ */
35
+ const HAS_RETURNING = /\bRETURNING\b/i;
36
+
37
+ /**
38
+ * Creates a Kysely-compatible SQLite database backed by `bun:sqlite`.
39
+ *
40
+ * @param path - File path or `":memory:"` for in-memory database.
41
+ * @param options - Options forwarded to `bun:sqlite`'s Database constructor.
42
+ * - `readonly`: Open in read-only mode.
43
+ * - `create`: Create the file if it doesn't exist (default: true).
44
+ * @returns An object implementing Kysely's `SqliteDatabase` interface.
45
+ */
46
+ export function createBunSqliteDatabase(
47
+ path: string,
48
+ options?: { readonly?: boolean; create?: boolean },
49
+ ): KyselySqliteDatabase {
50
+ const db = new BunDatabase(path, options);
51
+
52
+ return {
53
+ close(): void {
54
+ db.close();
55
+ },
56
+
57
+ prepare(sql: string): KyselySqliteStatement {
58
+ const stmt = db.prepare(sql);
59
+ const isReader = READER_PREFIXES.test(sql) || HAS_RETURNING.test(sql);
60
+
61
+ return {
62
+ get reader(): boolean {
63
+ return isReader;
64
+ },
65
+
66
+ all(parameters: ReadonlyArray<unknown>): unknown[] {
67
+ return stmt.all(...(parameters as any[]));
68
+ },
69
+
70
+ run(parameters: ReadonlyArray<unknown>): {
71
+ changes: number | bigint;
72
+ lastInsertRowid: number | bigint;
73
+ } {
74
+ return stmt.run(...(parameters as any[])) as {
75
+ changes: number | bigint;
76
+ lastInsertRowid: number | bigint;
77
+ };
78
+ },
79
+ };
80
+ },
81
+ };
82
+ }
@@ -1,13 +1,13 @@
1
- import {
1
+ import type {
2
2
  ColumnBuilderCallback,
3
3
  ColumnDataType,
4
4
  CreateTableBuilder,
5
- Dialect as KyselyDialect,
6
- Kysely,
7
5
  InsertObject,
8
6
  InsertQueryBuilder,
9
- Selection,
7
+ Kysely,
8
+ Dialect as KyselyDialect,
10
9
  SelectExpression,
10
+ Selection,
11
11
  Transaction,
12
12
  } from 'kysely';
13
13
 
@@ -64,7 +64,6 @@ export interface Dialect extends KyselyDialect {
64
64
  *
65
65
  * NOTE: the `returning` value must be formatted to return an insertId value.
66
66
  * ex. if the generated key is `id` the string should be `id as insertId`.
67
- * if the generated key is `watermark` the string should be `watermark as insertId`.
68
67
  *
69
68
  * @returns {InsertQueryBuilder} object to further modify the query or execute it.
70
69
  */
@@ -1,16 +1,18 @@
1
- import { Dialect } from './dialect.js';
2
- import {
1
+ import type { Dialect } from './dialect.js';
2
+ import type {
3
3
  AnyColumn,
4
+ ColumnBuilderCallback,
4
5
  ColumnDataType,
5
6
  CreateTableBuilder,
6
- ColumnBuilderCallback,
7
7
  InsertObject,
8
8
  InsertQueryBuilder,
9
+ Kysely,
9
10
  SelectExpression,
10
11
  Selection,
11
- Transaction,
12
- MysqlDialect as KyselyMysqlDialect,
13
- Kysely,
12
+ Transaction } from 'kysely';
13
+
14
+ import {
15
+ MysqlDialect as KyselyMysqlDialect
14
16
  } from 'kysely';
15
17
 
16
18
  export class MysqlDialect extends KyselyMysqlDialect implements Dialect {
@@ -1,15 +1,17 @@
1
- import { Dialect } from './dialect.js';
2
- import {
3
- ColumnDataType,
1
+ import type { Dialect } from './dialect.js';
2
+ import type {
4
3
  ColumnBuilderCallback,
4
+ ColumnDataType,
5
5
  CreateTableBuilder,
6
6
  InsertObject,
7
7
  InsertQueryBuilder,
8
8
  Kysely,
9
- PostgresDialect as KyselyPostgresDialect,
10
9
  SelectExpression,
11
10
  Selection,
12
- Transaction,
11
+ Transaction } from 'kysely';
12
+
13
+ import {
14
+ PostgresDialect as KyselyPostgresDialect
13
15
  } from 'kysely';
14
16
 
15
17
  export class PostgresDialect extends KyselyPostgresDialect implements Dialect {
@@ -51,7 +53,10 @@ export class PostgresDialect extends KyselyPostgresDialect implements Dialect {
51
53
  referenceColumnName: string,
52
54
  onDeleteAction: 'cascade' | 'no action' | 'restrict' | 'set null' | 'set default',
53
55
  ): CreateTableBuilder<TB & string> {
54
- return builder.addColumn(columnName, columnType, (col) => col.notNull().references(`${referenceTable}.${referenceColumnName}`).onDelete(onDeleteAction));
56
+ return builder.addColumn(
57
+ columnName, columnType,
58
+ (col) => col.notNull().references(`${referenceTable}.${referenceColumnName}`).onDelete(onDeleteAction),
59
+ );
55
60
  }
56
61
 
57
62
  insertThenReturnId<DB, TB extends keyof DB = keyof DB, SE extends SelectExpression<DB, TB & string> = any>(
@@ -1,15 +1,17 @@
1
- import { Dialect } from './dialect.js';
2
- import {
1
+ import type { Dialect } from './dialect.js';
2
+ import type {
3
3
  ColumnBuilderCallback,
4
4
  ColumnDataType,
5
5
  CreateTableBuilder,
6
- Kysely,
7
6
  InsertObject,
8
7
  InsertQueryBuilder,
8
+ Kysely,
9
9
  SelectExpression,
10
10
  Selection,
11
- SqliteDialect as KyselySqliteDialect,
12
- Transaction,
11
+ Transaction } from 'kysely';
12
+
13
+ import {
14
+ SqliteDialect as KyselySqliteDialect
13
15
  } from 'kysely';
14
16
 
15
17
  export class SqliteDialect extends KyselySqliteDialect implements Dialect {
@@ -58,7 +60,10 @@ export class SqliteDialect extends KyselySqliteDialect implements Dialect {
58
60
  referenceColumnName: string,
59
61
  onDeleteAction: 'cascade' | 'no action' | 'restrict' | 'set null' | 'set default',
60
62
  ): CreateTableBuilder<TB & string> {
61
- return builder.addColumn(columnName, columnType, (col) => col.notNull().references(`${referenceTable}.${referenceColumnName}`).onDelete(onDeleteAction));
63
+ return builder.addColumn(
64
+ columnName, columnType,
65
+ (col) => col.notNull().references(`${referenceTable}.${referenceColumnName}`).onDelete(onDeleteAction),
66
+ );
62
67
  }
63
68
 
64
69
  insertThenReturnId<DB, TB extends keyof DB = keyof DB, SE extends SelectExpression<DB, TB & string> = any>(
package/src/main.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  export * from './dialect/dialect.js';
2
+ export * from './dialect/bun-sqlite-adapter.js';
2
3
  export * from './dialect/mysql-dialect.js';
3
4
  export * from './dialect/postgres-dialect.js';
4
5
  export * from './dialect/sqlite-dialect.js';
5
6
  export * from './data-store-sql.js';
6
- export * from './event-log-sql.js';
7
+ export * from './state-index-sql.js';
7
8
  export * from './message-store-sql.js';
8
- export * from './resumable-task-store-sql.js';
9
+ export * from './resumable-task-store-sql.js';
10
+ export * from './smt-store-sql.js';
@@ -1,27 +1,29 @@
1
- import {
2
- DwnInterfaceName,
3
- DwnMethodName,
4
- executeUnlessAborted,
1
+ import type { Dialect } from './dialect/dialect.js';
2
+ import type { Transaction } from 'kysely';
3
+ import type { DwnDatabaseType, KeyValues } from './types.js';
4
+ import type {
5
5
  Filter,
6
6
  GenericMessage,
7
+ MessageSort,
7
8
  MessageStore,
8
9
  MessageStoreOptions,
9
- MessageSort,
10
10
  Pagination,
11
- SortDirection,
12
- PaginationCursor,
13
- } from '@enbox/dwn-sdk-js';
11
+ PaginationCursor } from '@enbox/dwn-sdk-js';
14
12
 
15
- import { Kysely, Transaction } from 'kysely';
16
- import { DwnDatabaseType, KeyValues } from './types.js';
17
13
  import * as block from 'multiformats/block';
18
14
  import * as cbor from '@ipld/dag-cbor';
19
- import { Dialect } from './dialect/dialect.js';
20
- import { executeWithRetryIfDatabaseIsLocked } from './utils/transaction.js';
15
+ import { executeWithTransaction } from './utils/transaction.js';
21
16
  import { extractTagsAndSanitizeIndexes } from './utils/sanitize.js';
22
17
  import { filterSelectQuery } from './utils/filter.js';
23
18
  import { sha256 } from 'multiformats/hashes/sha2';
24
19
  import { TagTables } from './utils/tags.js';
20
+ import {
21
+ DwnInterfaceName,
22
+ DwnMethodName,
23
+ executeUnlessAborted,
24
+ SortDirection
25
+ } from '@enbox/dwn-sdk-js';
26
+ import { Kysely, sql } from 'kysely';
25
27
 
26
28
 
27
29
  export class MessageStoreSql implements MessageStore {
@@ -31,7 +33,7 @@ export class MessageStoreSql implements MessageStore {
31
33
 
32
34
  constructor(dialect: Dialect) {
33
35
  this.#dialect = dialect;
34
- this.#tags = new TagTables(dialect, 'messageStoreMessages');
36
+ this.#tags = new TagTables(dialect);
35
37
  }
36
38
 
37
39
  async open(): Promise<void> {
@@ -57,7 +59,7 @@ export class MessageStoreSql implements MessageStore {
57
59
  .addColumn('parentId', 'varchar(60)')
58
60
  .addColumn('protocol', 'varchar(200)')
59
61
  .addColumn('protocolPath', 'varchar(200)')
60
- .addColumn('contextId', 'varchar(500)')
62
+ .addColumn('contextId', 'varchar(600)')
61
63
  .addColumn('schema', 'varchar(200)')
62
64
  .addColumn('author', 'varchar(255)')
63
65
  .addColumn('recipient', 'varchar(255)')
@@ -72,32 +74,48 @@ export class MessageStoreSql implements MessageStore {
72
74
  .addColumn('dataSize', 'integer')
73
75
  .addColumn('encodedData', 'text') // we optionally store encoded data if it is below a threshold
74
76
  .addColumn('attester', 'text')
75
- .addColumn('permissionGrantId', 'varchar(60)')
76
- .addColumn('latest', 'text'); // TODO: obsolete, remove once `dwn-sdk-js` tests are updated
77
+ .addColumn('permissionGrantId', 'varchar(60)');
77
78
 
78
79
  // Add columns that have dialect-specific constraints
79
80
  createMessagesTable = this.#dialect.addAutoIncrementingColumn(createMessagesTable, 'id', (col) => col.primaryKey());
80
81
  createMessagesTable = this.#dialect.addBlobColumn(createMessagesTable, 'encodedMessageBytes', (col) => col.notNull());
81
82
  await createMessagesTable.execute();
82
83
 
84
+ // add unique index for get() and delete() by messageCid — the most fundamental lookup path
85
+ await this.#db.schema
86
+ .createIndex('index_tenant_messageCid')
87
+ .on(messagesTableName)
88
+ .columns(['tenant', 'messageCid'])
89
+ .unique()
90
+ .execute();
91
+
83
92
  // add indexes to the table
84
93
  await this.createIndexes(this.#db, messagesTableName, [
85
- ['tenant'], // baseline protection to prevent full table scans across all tenants
86
94
  ['tenant', 'recordId'], // multiple uses, notably heavily depended by record chain construction for protocol authorization
95
+ ['tenant', 'entryId'], // used by fetchInitialRecordsWriteMessage in RecordsRead, RecordsQuery, and RecordsDelete
87
96
  ['tenant', 'parentId'], // used to walk down hierarchy of records, use cases include purging of records
88
- ['tenant', 'protocol', 'published', 'messageTimestamp'], // index used for basically every external query.
97
+ ['tenant', 'protocol', 'published', 'messageTimestamp'], // index used for basically every external query
89
98
  ['tenant', 'interface'], // mainly for fast fetch of ProtocolsConfigure for authorization, not needed if protocol was a DWN Record
90
- ['tenant', 'contextId', 'messageTimestamp'], // expected to be used for common query pattern
91
- ['tenant', 'permissionGrantId'], // for deleting grant-authorized messages though pending https://github.com/TBD54566975/dwn-sdk-js/issues/716
92
- // other potential indexes
93
- // ['tenant', 'author'],
94
- // ['tenant', 'recipient'],
95
- // ['tenant', 'schema', 'dataFormat'],
96
- // ['tenant', 'dateCreated'],
97
- // ['tenant', 'datePublished'],
98
- // ['tenant', 'messageCid'],
99
- // ['tenant', 'protocolPath'],
99
+ ['tenant', 'permissionGrantId'], // for deleting grant-authorized messages though pending https://github.com/enboxorg/enbox/issues/716
100
+ ['tenant', 'dateCreated'], // sort optimization for RecordsQuery with DateSort.CreatedAscending/Descending
101
+ ['tenant', 'datePublished'], // sort optimization for RecordsQuery with DateSort.PublishedAscending/Descending
100
102
  ]);
103
+
104
+ // contextId index created separately because MySQL requires a prefix length to fit within
105
+ // the 3072-byte InnoDB index key limit. contextId is varchar(600) × 4 bytes (utf8mb4) = 2400 bytes,
106
+ // which combined with tenant (255 × 4 = 1020) and messageTimestamp (30 × 4 = 120) = 3540 bytes,
107
+ // exceeding the limit. A prefix of 480 chars (1920 bytes) brings the total to 3060 bytes.
108
+ // contextId values only contain ASCII chars [a-zA-Z0-9/], so a 480-char prefix is sufficient
109
+ // to distinguish most records (covers ~8 nesting levels of 59-char CID segments).
110
+ if (this.#dialect.name === 'MySQL') {
111
+ await sql`CREATE INDEX index_tenant_contextId_messageTimestamp
112
+ ON ${sql.table(messagesTableName)} (tenant, contextId(480), messageTimestamp)`
113
+ .execute(this.#db);
114
+ } else {
115
+ await this.createIndexes(this.#db, messagesTableName, [
116
+ ['tenant', 'contextId', 'messageTimestamp'], // expected to be used for common query pattern
117
+ ]);
118
+ }
101
119
  }
102
120
 
103
121
  // create tags table
@@ -169,19 +187,19 @@ export class MessageStoreSql implements MessageStore {
169
187
  const getEncodedData = (message: GenericMessage): { message: GenericMessage, encodedData: string|null} => {
170
188
  let encodedData: string|null = null;
171
189
  if (message.descriptor.interface === DwnInterfaceName.Records && message.descriptor.method === DwnMethodName.Write) {
172
- const data = (message as any).encodedData as string|undefined;
173
- if(data) {
174
- delete (message as any).encodedData;
190
+ const data = message.encodedData;
191
+ if (data) {
192
+ delete message.encodedData;
175
193
  encodedData = data;
176
194
  }
177
195
  }
178
196
  return { message, encodedData };
179
197
  };
180
198
 
181
- const { message: messageToProcess, encodedData} = getEncodedData(message);
199
+ const { message: messageToProcess, encodedData } = getEncodedData(message);
182
200
 
183
201
  const encodedMessageBlock = await executeUnlessAborted(
184
- block.encode({ value: messageToProcess, codec: cbor, hasher: sha256}),
202
+ block.encode({ value: messageToProcess, codec: cbor, hasher: sha256 }),
185
203
  options?.signal
186
204
  );
187
205
 
@@ -192,7 +210,7 @@ export class MessageStoreSql implements MessageStore {
192
210
  // if any of these inserts would throw, the whole transaction would be rolled back.
193
211
  // otherwise it is committed.
194
212
  const putMessageOperation = this.constructPutMessageOperation({ tenant, messageCid, encodedMessageBytes, encodedData, indexes });
195
- await executeWithRetryIfDatabaseIsLocked(this.#db, putMessageOperation);
213
+ await executeWithTransaction(this.#db, putMessageOperation);
196
214
  }
197
215
 
198
216
  /**
@@ -297,9 +315,9 @@ export class MessageStoreSql implements MessageStore {
297
315
  // filter sanitization takes place within `filterSelectQuery`
298
316
  query = filterSelectQuery(filters, query);
299
317
 
300
- if(pagination?.cursor !== undefined) {
318
+ if (pagination?.cursor !== undefined) {
301
319
  // currently the sort property is explicitly either `dateCreated` | `messageTimestamp` | `datePublished` which are all strings
302
- // TODO: https://github.com/TBD54566975/dwn-sdk-js/issues/664 to handle the edge case
320
+ // TODO: https://github.com/enboxorg/enbox/issues/664 to handle the edge case
303
321
  const cursorValue = pagination.cursor.value as string;
304
322
  const cursorMessageId = pagination.cursor.messageCid;
305
323
 
@@ -312,7 +330,7 @@ export class MessageStoreSql implements MessageStore {
312
330
 
313
331
  const orderDirection = sortDirection === SortDirection.Ascending ? 'asc' : 'desc';
314
332
  // sorting by the provided sort property, the tiebreak is always in ascending order regardless of sort
315
- query = query
333
+ query = query
316
334
  .orderBy(sortProperty, orderDirection)
317
335
  .orderBy('messageCid', orderDirection);
318
336
 
@@ -331,6 +349,33 @@ export class MessageStoreSql implements MessageStore {
331
349
  return this.processPaginationResults(results, sortProperty, pagination?.limit, options);
332
350
  }
333
351
 
352
+ async count(
353
+ tenant: string,
354
+ filters: Filter[],
355
+ messageSort?: MessageSort,
356
+ options?: MessageStoreOptions
357
+ ): Promise<number> {
358
+ if (!this.#db) {
359
+ throw new Error(
360
+ 'Connection to database not open. Call `open` before using `count`.'
361
+ );
362
+ }
363
+
364
+ options?.signal?.throwIfAborted();
365
+
366
+ let query = this.#db
367
+ .selectFrom('messageStoreMessages')
368
+ .leftJoin('messageStoreRecordsTags', 'messageStoreRecordsTags.messageInsertId', 'messageStoreMessages.id')
369
+ .select(sql<number>`count(distinct ${sql.ref('messageStoreMessages.messageCid')})`.as('count'))
370
+ .where('tenant', '=', tenant);
371
+
372
+ query = filterSelectQuery(filters, query);
373
+
374
+ const result = await executeUnlessAborted(query.executeTakeFirstOrThrow(), options?.signal);
375
+
376
+ return Number(result.count);
377
+ }
378
+
334
379
  async delete(
335
380
  tenant: string,
336
381
  cid: string,
@@ -382,9 +427,9 @@ export class MessageStoreSql implements MessageStore {
382
427
  const message = decodedBlock.value as GenericMessage;
383
428
  // If encodedData is stored within the MessageStore we include it in the response.
384
429
  // We store encodedData when the data is below a certain threshold.
385
- // https://github.com/TBD54566975/dwn-sdk-js/pull/456
430
+ // https://github.com/enboxorg/enbox/pull/456
386
431
  if (message !== undefined && encodedData !== undefined && encodedData !== null) {
387
- (message as any).encodedData = encodedData;
432
+ message.encodedData = encodedData;
388
433
  }
389
434
  return message;
390
435
  }
@@ -427,14 +472,14 @@ export class MessageStoreSql implements MessageStore {
427
472
  private extractSortProperties(
428
473
  messageSort?: MessageSort
429
474
  ):{ property: 'dateCreated' | 'datePublished' | 'messageTimestamp', direction: SortDirection } {
430
- if(messageSort?.dateCreated !== undefined) {
431
- return { property: 'dateCreated', direction: messageSort.dateCreated };
432
- } else if(messageSort?.datePublished !== undefined) {
433
- return { property: 'datePublished', direction: messageSort.datePublished };
475
+ if (messageSort?.dateCreated !== undefined) {
476
+ return { property: 'dateCreated', direction: messageSort.dateCreated };
477
+ } else if (messageSort?.datePublished !== undefined) {
478
+ return { property: 'datePublished', direction: messageSort.datePublished };
434
479
  } else if (messageSort?.messageTimestamp !== undefined) {
435
- return { property: 'messageTimestamp', direction: messageSort.messageTimestamp };
480
+ return { property: 'messageTimestamp', direction: messageSort.messageTimestamp };
436
481
  } else {
437
- return { property: 'messageTimestamp', direction: SortDirection.Ascending };
482
+ return { property: 'messageTimestamp', direction: SortDirection.Ascending };
438
483
  }
439
484
  }
440
485
  }
@@ -1,8 +1,10 @@
1
- import { DwnDatabaseType } from './types.js';
2
- import { Dialect } from './dialect/dialect.js';
3
- import { executeWithRetryIfDatabaseIsLocked } from './utils/transaction.js';
1
+ import type { Dialect } from './dialect/dialect.js';
2
+ import type { DwnDatabaseType } from './types.js';
3
+ import type { ManagedResumableTask, ResumableTaskStore } from '@enbox/dwn-sdk-js';
4
+
5
+ import { Cid } from '@enbox/dwn-sdk-js';
6
+ import { executeWithTransaction } from './utils/transaction.js';
4
7
  import { Kysely } from 'kysely';
5
- import { Cid, ManagedResumableTask, ResumableTaskStore } from '@enbox/dwn-sdk-js';
6
8
 
7
9
  export class ResumableTaskStoreSql implements ResumableTaskStore {
8
10
  private static readonly taskTimeoutInSeconds = 60;
@@ -30,7 +32,7 @@ export class ResumableTaskStoreSql implements ResumableTaskStore {
30
32
 
31
33
  // else create the table and corresponding indexes
32
34
 
33
- let table = this.#db.schema
35
+ const table = this.#db.schema
34
36
  .createTable(tableName)
35
37
  .ifNotExists() // kept to show supported by all dialects in contrast to ifNotExists() below, though not needed due to hasTable() check above
36
38
  .addColumn('id', 'varchar(255)', (col) => col.primaryKey())
@@ -83,7 +85,7 @@ export class ResumableTaskStoreSql implements ResumableTaskStore {
83
85
 
84
86
  let tasks: DwnDatabaseType['resumableTasks'][] = [];
85
87
 
86
- const operation = async (transaction) => {
88
+ const operation = async (transaction): Promise<void> => {
87
89
  tasks = await transaction
88
90
  .selectFrom('resumableTasks')
89
91
  .selectAll()
@@ -101,7 +103,7 @@ export class ResumableTaskStoreSql implements ResumableTaskStore {
101
103
  }
102
104
  };
103
105
 
104
- await executeWithRetryIfDatabaseIsLocked(this.#db, operation);
106
+ await executeWithTransaction(this.#db, operation);
105
107
 
106
108
  const tasksToReturn = tasks.map((task) => {
107
109
  return {