@remit/drizzle-service 0.0.101 → 0.0.103
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/package.json +1 -1
- package/src/batch.sqlite.test.ts +4 -4
- package/src/batch.ts +47 -21
- package/src/databases.ts +24 -6
- package/src/db.ts +20 -5
- package/src/index.ts +12 -0
- package/src/repair/junk-only-address.sqlite.test.ts +7 -2
- package/src/repos/account.conformance.sqlite.test.ts +6 -2
- package/src/repos/calendar-collection.ts +2 -2
- package/src/repos/calendar-default.sqlite.test.ts +6 -2
- package/src/repos/calendar-event-index.ts +2 -2
- package/src/repos/calendar-feed-token.ts +2 -2
- package/src/repos/calendar-object.ts +2 -2
- package/src/repos/calendar-put.sqlite.test.ts +6 -2
- package/src/repos/calendar-store.ts +36 -40
- package/src/repos/calendar-suggestion.ts +5 -4
- package/src/repos/cascade-delete.test.ts +3 -2
- package/src/repos/cascade-delete.ts +38 -35
- package/src/repos/envelope.sqlite.test.ts +3 -2
- package/src/repos/envelope.test.ts +3 -2
- package/src/repos/envelope.ts +19 -13
- package/src/repos/filter.ts +2 -2
- package/src/repos/i4-account-config.test.ts +10 -6
- package/src/repos/i4-account-config.ts +12 -13
- package/src/repos/i4-account-export-request.ts +2 -2
- package/src/repos/i4-account-setting.ts +6 -5
- package/src/repos/i4-account.test.ts +12 -8
- package/src/repos/i4-account.ts +10 -13
- package/src/repos/i4-address-junk-move.sqlite.test.ts +7 -2
- package/src/repos/i4-address.ts +17 -7
- package/src/repos/i4-config-import.ts +2 -2
- package/src/repos/i4-mailbox-lock.ts +2 -2
- package/src/repos/i4-mailbox-special-use.sqlite.test.ts +12 -10
- package/src/repos/i4-mailbox-special-use.ts +34 -44
- package/src/repos/i4-mailbox.sqlite.test.ts +12 -10
- package/src/repos/i4-mailbox.test.ts +15 -14
- package/src/repos/i4-mailbox.ts +19 -12
- package/src/repos/i4-message-flag-push.ts +2 -2
- package/src/repos/i4-message-placement-move.ts +2 -2
- package/src/repos/i4-organize-job-request.ts +2 -2
- package/src/repos/i4-outbox-attachment.test.ts +7 -2
- package/src/repos/i4-outbox-attachment.ts +14 -10
- package/src/repos/i4-outbox-message.ts +7 -6
- package/src/repos/i4-sender-signer-standing.ts +2 -2
- package/src/repos/in-list.sqlite.test.ts +61 -0
- package/src/repos/in-list.ts +14 -0
- package/src/repos/label.ts +2 -2
- package/src/repos/message-flag.ts +8 -10
- package/src/repos/message-label.ts +5 -4
- package/src/repos/message.sqlite.test.ts +3 -3
- package/src/repos/message.test.ts +6 -5
- package/src/repos/message.ts +30 -24
- package/src/repos/quarantine.ts +2 -2
- package/src/repos/sync-warning.ts +5 -4
- package/src/repos/thread-message-body-muted.sqlite.test.ts +7 -3
- package/src/repos/thread-message-field-terms.sqlite.test.ts +7 -6
- package/src/repos/thread-message.sqlite.test.ts +7 -3
- package/src/repos/thread-message.test.ts +22 -18
- package/src/repos/thread-message.ts +16 -7
- package/src/search/thread-message-search.sqlite.test.ts +8 -3
- package/src/search/thread-message-search.ts +13 -8
- package/src/sqlite-repositories.ts +48 -32
package/package.json
CHANGED
package/src/batch.sqlite.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { after, before, describe, test } from "node:test";
|
|
3
3
|
import { eq } from "drizzle-orm";
|
|
4
|
-
import {
|
|
4
|
+
import { syncBatch } from "./batch.js";
|
|
5
5
|
import { calendarTable } from "./schema.js";
|
|
6
6
|
import { createSqliteTestDb, type SqliteTestDb } from "./test-db-sqlite.js";
|
|
7
7
|
|
|
@@ -36,7 +36,7 @@ describe("a batch on better-sqlite3", () => {
|
|
|
36
36
|
const stored = () => db.select().from(calendarTable);
|
|
37
37
|
|
|
38
38
|
test("answers each statement in order, reads seeing the writes before them", async () => {
|
|
39
|
-
const [inserted, read] = await
|
|
39
|
+
const [inserted, read] = await syncBatch(db)([
|
|
40
40
|
db.insert(calendarTable).values(collection("first")).returning(),
|
|
41
41
|
db
|
|
42
42
|
.select()
|
|
@@ -58,7 +58,7 @@ describe("a batch on better-sqlite3", () => {
|
|
|
58
58
|
const before = await stored();
|
|
59
59
|
|
|
60
60
|
await assert.rejects(
|
|
61
|
-
|
|
61
|
+
syncBatch(db)([
|
|
62
62
|
db.insert(calendarTable).values(collection("second")),
|
|
63
63
|
db
|
|
64
64
|
.update(calendarTable)
|
|
@@ -73,6 +73,6 @@ describe("a batch on better-sqlite3", () => {
|
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
test("an empty batch writes nothing and answers nothing", async () => {
|
|
76
|
-
assert.deepEqual(await
|
|
76
|
+
assert.deepEqual(await syncBatch(db)([]), []);
|
|
77
77
|
});
|
|
78
78
|
});
|
package/src/batch.ts
CHANGED
|
@@ -3,6 +3,26 @@ import type { Db } from "./db.js";
|
|
|
3
3
|
|
|
4
4
|
export type SqliteBatchItem = BatchItem<"sqlite">;
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* The one write port a repository is handed for a write set, bound to one
|
|
8
|
+
* database. It has the shape of D1's `batch()`: every statement commits
|
|
9
|
+
* together or not at all, and none of them reads another's result mid-flight.
|
|
10
|
+
* A write that needs a value reads it before the batch and is written so a
|
|
11
|
+
* stale read loses safely (a conditional `WHERE`, an upsert, a deterministic
|
|
12
|
+
* id).
|
|
13
|
+
*/
|
|
14
|
+
export type WriteBatch = <T extends readonly SqliteBatchItem[]>(
|
|
15
|
+
items: T,
|
|
16
|
+
) => Promise<BatchResponse<T>>;
|
|
17
|
+
|
|
18
|
+
type NonEmptyBatch = readonly [SqliteBatchItem, ...SqliteBatchItem[]];
|
|
19
|
+
|
|
20
|
+
export interface BatchingDatabase {
|
|
21
|
+
batch<U extends SqliteBatchItem, T extends Readonly<[U, ...U[]]>>(
|
|
22
|
+
batch: T,
|
|
23
|
+
): Promise<BatchResponse<T>>;
|
|
24
|
+
}
|
|
25
|
+
|
|
6
26
|
interface SyncPreparable {
|
|
7
27
|
_prepare(): { execute(): { sync(): unknown } };
|
|
8
28
|
}
|
|
@@ -12,6 +32,10 @@ const isSyncPreparable = (value: unknown): value is SyncPreparable =>
|
|
|
12
32
|
value !== null &&
|
|
13
33
|
typeof Reflect.get(value, "_prepare") === "function";
|
|
14
34
|
|
|
35
|
+
const isNonEmpty = <T extends readonly SqliteBatchItem[]>(
|
|
36
|
+
items: T,
|
|
37
|
+
): items is T & NonEmptyBatch => items.length > 0;
|
|
38
|
+
|
|
15
39
|
const runSync = (item: SqliteBatchItem): unknown => {
|
|
16
40
|
if (!isSyncPreparable(item)) {
|
|
17
41
|
throw new TypeError("batch item is not a drizzle sqlite query");
|
|
@@ -19,28 +43,30 @@ const runSync = (item: SqliteBatchItem): unknown => {
|
|
|
19
43
|
return item._prepare().execute().sync();
|
|
20
44
|
};
|
|
21
45
|
|
|
46
|
+
const emptyResponse = <
|
|
47
|
+
T extends readonly SqliteBatchItem[],
|
|
48
|
+
>(): BatchResponse<T> => [] as unknown as BatchResponse<T>;
|
|
49
|
+
|
|
22
50
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* (a conditional `WHERE`, an upsert, a deterministic id).
|
|
27
|
-
*
|
|
28
|
-
* On better-sqlite3 the batch is one synchronous IMMEDIATE transaction: it
|
|
29
|
-
* never yields the event loop, so no other write on the shared connection lands
|
|
30
|
-
* inside it, and IMMEDIATE takes the file's write lock before the first
|
|
51
|
+
* A batch as one synchronous transaction. On better-sqlite3 it is IMMEDIATE:
|
|
52
|
+
* it never yields the event loop, so no other write on the shared connection
|
|
53
|
+
* lands inside it, and it takes the file's write lock before the first
|
|
31
54
|
* statement so a writer in another process waits out the busy timeout. On a
|
|
32
55
|
* Durable Object's `durable-sqlite` handle it is one `transactionSync`.
|
|
33
56
|
*/
|
|
34
|
-
export const
|
|
35
|
-
TSchema extends Record<string, unknown
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
export const syncBatch =
|
|
58
|
+
<TSchema extends Record<string, unknown>>(db: Db<TSchema>): WriteBatch =>
|
|
59
|
+
async (items) => {
|
|
60
|
+
if (items.length === 0) return emptyResponse();
|
|
61
|
+
const results = db.transaction(() => items.map(runSync), {
|
|
62
|
+
behavior: "immediate",
|
|
63
|
+
});
|
|
64
|
+
return results as unknown as BatchResponse<typeof items>;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const asyncBatch =
|
|
68
|
+
(db: BatchingDatabase): WriteBatch =>
|
|
69
|
+
async (items) => {
|
|
70
|
+
if (!isNonEmpty(items)) return emptyResponse();
|
|
71
|
+
return db.batch(items);
|
|
72
|
+
};
|
package/src/databases.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
messageDataSchema,
|
|
5
|
-
} from "./schema/message-data.js";
|
|
1
|
+
import { syncBatch } from "./batch.js";
|
|
2
|
+
import type { Db, SqliteStore } from "./db.js";
|
|
3
|
+
import { messageDataSchema } from "./schema/message-data.js";
|
|
6
4
|
import { createSqliteDatabase } from "./sqlite-client.js";
|
|
7
5
|
|
|
8
6
|
// The three databases a self-host instance runs on (#1420). The app database
|
|
@@ -12,10 +10,30 @@ import { createSqliteDatabase } from "./sqlite-client.js";
|
|
|
12
10
|
// rebuilt from the mail database, so it never commits together with it.
|
|
13
11
|
export interface SqliteDatabases {
|
|
14
12
|
app: Db<Record<string, unknown>>;
|
|
15
|
-
mail: Db<
|
|
13
|
+
mail: Db<Record<string, unknown>>;
|
|
16
14
|
search: Db<Record<string, unknown>>;
|
|
17
15
|
}
|
|
18
16
|
|
|
17
|
+
export interface SqliteStores {
|
|
18
|
+
app: SqliteStore<Record<string, unknown>>;
|
|
19
|
+
mail: SqliteStore<Record<string, unknown>>;
|
|
20
|
+
search: SqliteStore<Record<string, unknown>>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const syncStore = <TSchema extends Record<string, unknown>>(
|
|
24
|
+
db: Db<TSchema>,
|
|
25
|
+
): SqliteStore<TSchema> => ({ db, batch: syncBatch(db) });
|
|
26
|
+
|
|
27
|
+
export const syncStores = ({
|
|
28
|
+
app,
|
|
29
|
+
mail,
|
|
30
|
+
search,
|
|
31
|
+
}: SqliteDatabases): SqliteStores => ({
|
|
32
|
+
app: syncStore(app),
|
|
33
|
+
mail: syncStore(mail),
|
|
34
|
+
search: syncStore(search),
|
|
35
|
+
});
|
|
36
|
+
|
|
19
37
|
export interface SqliteDatabasePaths {
|
|
20
38
|
app: string;
|
|
21
39
|
mail: string;
|
package/src/db.ts
CHANGED
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
import type { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core";
|
|
2
|
+
import type { WriteBatch } from "./batch.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `durable-sqlite` handle satisfies it too, so its adapter constructs the same
|
|
8
|
-
* repositories over its own storage.
|
|
5
|
+
* A synchronous SQLite handle: better-sqlite3, or a Cloudflare Durable
|
|
6
|
+
* Object's `durable-sqlite`. Only `syncBatch` and the better-sqlite3
|
|
7
|
+
* composition name it; a repository never does.
|
|
9
8
|
*/
|
|
10
9
|
export type Db<TSchema extends Record<string, unknown>> = BaseSQLiteDatabase<
|
|
11
10
|
"sync",
|
|
12
11
|
unknown,
|
|
13
12
|
TSchema
|
|
14
13
|
>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The handle a repository runs on: any SQLite driver, synchronous or not
|
|
17
|
+
* (better-sqlite3, Durable Object storage, D1, libsql). It has no
|
|
18
|
+
* `transaction` and no `batch`, so a repository writes a set through the
|
|
19
|
+
* injected `WriteBatch` and cannot open an interactive transaction (C5).
|
|
20
|
+
*/
|
|
21
|
+
export type RepositoryDb<TSchema extends Record<string, unknown>> = Omit<
|
|
22
|
+
BaseSQLiteDatabase<"sync" | "async", unknown, TSchema>,
|
|
23
|
+
"transaction" | "batch"
|
|
24
|
+
>;
|
|
25
|
+
|
|
26
|
+
export interface SqliteStore<TSchema extends Record<string, unknown>> {
|
|
27
|
+
db: RepositoryDb<TSchema>;
|
|
28
|
+
batch: WriteBatch;
|
|
29
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
|
+
export {
|
|
2
|
+
asyncBatch,
|
|
3
|
+
type BatchingDatabase,
|
|
4
|
+
type SqliteBatchItem,
|
|
5
|
+
syncBatch,
|
|
6
|
+
type WriteBatch,
|
|
7
|
+
} from "./batch.js";
|
|
1
8
|
export {
|
|
2
9
|
hasSqliteDatabaseEnv,
|
|
3
10
|
openSqliteDatabases,
|
|
4
11
|
SQLITE_DATABASE_ENV,
|
|
5
12
|
type SqliteDatabasePaths,
|
|
6
13
|
type SqliteDatabases,
|
|
14
|
+
type SqliteStores,
|
|
7
15
|
sqliteDatabasePathsFromEnv,
|
|
16
|
+
syncStore,
|
|
17
|
+
syncStores,
|
|
8
18
|
} from "./databases.js";
|
|
19
|
+
export type { RepositoryDb, SqliteStore } from "./db.js";
|
|
9
20
|
export { CreateFailedConflictError, NotFoundError } from "./error.js";
|
|
10
21
|
export { CalendarCollectionRepo } from "./repos/calendar-collection.js";
|
|
11
22
|
export { CalendarEventIndexRepo } from "./repos/calendar-event-index.js";
|
|
@@ -79,6 +90,7 @@ export {
|
|
|
79
90
|
type SqliteClientOptions,
|
|
80
91
|
} from "./sqlite-client.js";
|
|
81
92
|
export {
|
|
93
|
+
composeSqliteRepositories,
|
|
82
94
|
createSqliteRepositories,
|
|
83
95
|
type SqliteRepositories,
|
|
84
96
|
} from "./sqlite-repositories.js";
|
|
@@ -5,7 +5,8 @@ import { CanonicalMailboxRole } from "@remit/domain-enums";
|
|
|
5
5
|
import Database from "better-sqlite3";
|
|
6
6
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
7
7
|
import { AccountSettingRepo } from "../repos/i4-account-setting.js";
|
|
8
|
-
import { MailboxSpecialUseRepo } from "../repos/i4-mailbox-special-use.js";
|
|
8
|
+
import type { MailboxSpecialUseRepo } from "../repos/i4-mailbox-special-use.js";
|
|
9
|
+
import { createSqliteRepositories } from "../sqlite-repositories.js";
|
|
9
10
|
import { applyMigrationSet } from "../test-shipped-sqlite-schema.js";
|
|
10
11
|
import {
|
|
11
12
|
type JunkOnlyRepairClient,
|
|
@@ -181,7 +182,11 @@ describe("addresses standing only on mail in Junk", () => {
|
|
|
181
182
|
before(() => {
|
|
182
183
|
sqlite = new Database(":memory:");
|
|
183
184
|
const db = drizzle(sqlite);
|
|
184
|
-
specialUse =
|
|
185
|
+
specialUse = createSqliteRepositories({
|
|
186
|
+
app: db,
|
|
187
|
+
mail: db,
|
|
188
|
+
search: db,
|
|
189
|
+
}).mailboxSpecialUse;
|
|
185
190
|
accountSetting = new AccountSettingRepo(db as never);
|
|
186
191
|
applyMigrationSet(sqlite, "app");
|
|
187
192
|
applyMigrationSet(sqlite, "mail");
|
|
@@ -2,8 +2,8 @@ import { accountRepositoryConformance } from "@remit/data-ports/conformance";
|
|
|
2
2
|
import { NotFoundError } from "../error.js";
|
|
3
3
|
import { randomId } from "../id.js";
|
|
4
4
|
import { accountTable } from "../schema.js";
|
|
5
|
+
import { createSqliteRepositories } from "../sqlite-repositories.js";
|
|
5
6
|
import { createSqliteTestDb } from "../test-db-sqlite.js";
|
|
6
|
-
import { AccountRepo } from "./i4-account.js";
|
|
7
7
|
|
|
8
8
|
let close: (() => Promise<void>) | undefined;
|
|
9
9
|
|
|
@@ -13,7 +13,11 @@ accountRepositoryConformance({
|
|
|
13
13
|
accounts: accountTable,
|
|
14
14
|
});
|
|
15
15
|
close = closeDb;
|
|
16
|
-
return
|
|
16
|
+
return createSqliteRepositories({
|
|
17
|
+
app: db,
|
|
18
|
+
mail: db,
|
|
19
|
+
search: db,
|
|
20
|
+
}).account;
|
|
17
21
|
},
|
|
18
22
|
teardown: async () => {
|
|
19
23
|
await close?.();
|
|
@@ -9,11 +9,11 @@ import {
|
|
|
9
9
|
normalizeCalendarUrlSegment,
|
|
10
10
|
} from "@remit/data-ports/id";
|
|
11
11
|
import { and, asc, eq, sql } from "drizzle-orm";
|
|
12
|
-
import type {
|
|
12
|
+
import type { RepositoryDb } from "../db.js";
|
|
13
13
|
import { NotFoundError } from "../error.js";
|
|
14
14
|
import { calendarTable } from "../schema.js";
|
|
15
15
|
|
|
16
|
-
type DB =
|
|
16
|
+
type DB = RepositoryDb<Record<string, unknown>>;
|
|
17
17
|
|
|
18
18
|
function rowToCalendar(
|
|
19
19
|
row: typeof calendarTable.$inferSelect,
|
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
calendarObjectTable,
|
|
9
9
|
calendarTable,
|
|
10
10
|
} from "../schema.js";
|
|
11
|
+
import { createSqliteRepositories } from "../sqlite-repositories.js";
|
|
11
12
|
import { createSqliteTestDb } from "../test-db-sqlite.js";
|
|
12
13
|
import { CalendarCollectionRepo } from "./calendar-collection.js";
|
|
13
|
-
import { DrizzleCalendarStore } from "./calendar-store.js";
|
|
14
14
|
|
|
15
15
|
const ACCOUNT_CONFIG_ID = "account-config-default";
|
|
16
16
|
|
|
@@ -27,7 +27,11 @@ describe("provisioning the default calendar", () => {
|
|
|
27
27
|
});
|
|
28
28
|
db = created.db as unknown as Db<Record<string, unknown>>;
|
|
29
29
|
close = created.close;
|
|
30
|
-
store =
|
|
30
|
+
store = createSqliteRepositories({
|
|
31
|
+
app: db,
|
|
32
|
+
mail: db,
|
|
33
|
+
search: db,
|
|
34
|
+
}).calendarStore;
|
|
31
35
|
});
|
|
32
36
|
|
|
33
37
|
after(() => close());
|
|
@@ -4,10 +4,10 @@ import type {
|
|
|
4
4
|
ICalendarEventIndexRepository,
|
|
5
5
|
} from "@remit/data-ports";
|
|
6
6
|
import { and, asc, eq, gte, lt } from "drizzle-orm";
|
|
7
|
-
import type {
|
|
7
|
+
import type { RepositoryDb } from "../db.js";
|
|
8
8
|
import { calendarEventIndexTable } from "../schema.js";
|
|
9
9
|
|
|
10
|
-
type DB =
|
|
10
|
+
type DB = RepositoryDb<Record<string, unknown>>;
|
|
11
11
|
|
|
12
12
|
function rowToOccurrence(
|
|
13
13
|
row: typeof calendarEventIndexTable.$inferSelect,
|
|
@@ -5,10 +5,10 @@ import type {
|
|
|
5
5
|
} from "@remit/data-ports";
|
|
6
6
|
import { deriveCalendarFeedTokenId } from "@remit/data-ports/id";
|
|
7
7
|
import { and, eq } from "drizzle-orm";
|
|
8
|
-
import type {
|
|
8
|
+
import type { RepositoryDb } from "../db.js";
|
|
9
9
|
import { calendarFeedTokenTable } from "../schema.js";
|
|
10
10
|
|
|
11
|
-
type DB =
|
|
11
|
+
type DB = RepositoryDb<Record<string, unknown>>;
|
|
12
12
|
|
|
13
13
|
function rowToFeedToken(
|
|
14
14
|
row: typeof calendarFeedTokenTable.$inferSelect,
|
|
@@ -5,11 +5,11 @@ import type {
|
|
|
5
5
|
} from "@remit/data-ports";
|
|
6
6
|
import { deriveCalendarObjectId } from "@remit/data-ports/id";
|
|
7
7
|
import { and, asc, eq, gt, gte, lt, ne } from "drizzle-orm";
|
|
8
|
-
import type {
|
|
8
|
+
import type { RepositoryDb } from "../db.js";
|
|
9
9
|
import { NotFoundError } from "../error.js";
|
|
10
10
|
import { calendarObjectTable } from "../schema.js";
|
|
11
11
|
|
|
12
|
-
type DB =
|
|
12
|
+
type DB = RepositoryDb<Record<string, unknown>>;
|
|
13
13
|
|
|
14
14
|
function rowToCalendarObject(
|
|
15
15
|
row: typeof calendarObjectTable.$inferSelect,
|
|
@@ -16,8 +16,8 @@ import {
|
|
|
16
16
|
calendarObjectTable,
|
|
17
17
|
calendarTable,
|
|
18
18
|
} from "../schema.js";
|
|
19
|
+
import { createSqliteRepositories } from "../sqlite-repositories.js";
|
|
19
20
|
import { createSqliteTestDb } from "../test-db-sqlite.js";
|
|
20
|
-
import { DrizzleCalendarStore } from "./calendar-store.js";
|
|
21
21
|
|
|
22
22
|
const ACCOUNT_CONFIG_ID = "account-config-1";
|
|
23
23
|
|
|
@@ -77,7 +77,11 @@ describe("the calendar write path against sqlite", () => {
|
|
|
77
77
|
});
|
|
78
78
|
db = created.db as unknown as Db<Record<string, unknown>>;
|
|
79
79
|
close = created.close;
|
|
80
|
-
store =
|
|
80
|
+
store = createSqliteRepositories({
|
|
81
|
+
app: db,
|
|
82
|
+
mail: db,
|
|
83
|
+
search: db,
|
|
84
|
+
}).calendarStore;
|
|
81
85
|
const collection = await provisionDefaultCalendar(store, ACCOUNT_CONFIG_ID);
|
|
82
86
|
calendarId = collection.calendarId;
|
|
83
87
|
});
|
|
@@ -2,56 +2,67 @@ import type {
|
|
|
2
2
|
CalendarRepositories,
|
|
3
3
|
CalendarWrite,
|
|
4
4
|
ICalendarStore,
|
|
5
|
+
ICalendarSuggestionRepository,
|
|
6
|
+
SettleCalendarSuggestionWrite,
|
|
5
7
|
} from "@remit/data-ports";
|
|
6
8
|
import { deriveCalendarObjectId } from "@remit/data-ports/id";
|
|
7
9
|
import { and, eq, exists, sql } from "drizzle-orm";
|
|
8
|
-
import {
|
|
9
|
-
import type {
|
|
10
|
+
import type { SqliteBatchItem, WriteBatch } from "../batch.js";
|
|
11
|
+
import type { RepositoryDb, SqliteStore } from "../db.js";
|
|
10
12
|
import {
|
|
11
13
|
calendarEventIndexTable,
|
|
12
14
|
calendarFeedTokenTable,
|
|
13
15
|
calendarObjectTable,
|
|
14
|
-
calendarSuggestionTable,
|
|
15
16
|
calendarTable,
|
|
16
17
|
} from "../schema.js";
|
|
17
18
|
import { CalendarCollectionRepo } from "./calendar-collection.js";
|
|
18
19
|
import { CalendarEventIndexRepo } from "./calendar-event-index.js";
|
|
19
20
|
import { CalendarFeedTokenRepo } from "./calendar-feed-token.js";
|
|
20
21
|
import { CalendarObjectRepo } from "./calendar-object.js";
|
|
21
|
-
import { CalendarSuggestionRepo } from "./calendar-suggestion.js";
|
|
22
22
|
|
|
23
|
-
type DB =
|
|
23
|
+
type DB = RepositoryDb<Record<string, unknown>>;
|
|
24
|
+
|
|
25
|
+
type CalendarBatchWrite = Exclude<CalendarWrite, SettleCalendarSuggestionWrite>;
|
|
26
|
+
|
|
27
|
+
const isSettlement = (
|
|
28
|
+
write: CalendarWrite,
|
|
29
|
+
): write is SettleCalendarSuggestionWrite => write.kind === "SettleSuggestion";
|
|
30
|
+
|
|
31
|
+
const isBatchWrite = (write: CalendarWrite): write is CalendarBatchWrite =>
|
|
32
|
+
!isSettlement(write);
|
|
24
33
|
|
|
25
34
|
export class DrizzleCalendarStore implements ICalendarStore {
|
|
26
35
|
readonly repos: CalendarRepositories;
|
|
27
36
|
|
|
37
|
+
private readonly db: DB;
|
|
38
|
+
private readonly batch: WriteBatch;
|
|
39
|
+
|
|
28
40
|
constructor(
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
store: SqliteStore<Record<string, unknown>>,
|
|
42
|
+
calendarSuggestion: ICalendarSuggestionRepository,
|
|
31
43
|
) {
|
|
44
|
+
this.db = store.db;
|
|
45
|
+
this.batch = store.batch;
|
|
32
46
|
this.repos = {
|
|
33
|
-
calendarCollection: new CalendarCollectionRepo(db),
|
|
34
|
-
calendarObject: new CalendarObjectRepo(db),
|
|
35
|
-
calendarEventIndex: new CalendarEventIndexRepo(db),
|
|
36
|
-
calendarSuggestion
|
|
37
|
-
calendarFeedToken: new CalendarFeedTokenRepo(db),
|
|
47
|
+
calendarCollection: new CalendarCollectionRepo(store.db),
|
|
48
|
+
calendarObject: new CalendarObjectRepo(store.db),
|
|
49
|
+
calendarEventIndex: new CalendarEventIndexRepo(store.db),
|
|
50
|
+
calendarSuggestion,
|
|
51
|
+
calendarFeedToken: new CalendarFeedTokenRepo(store.db),
|
|
38
52
|
};
|
|
39
53
|
}
|
|
40
54
|
|
|
41
55
|
async commit(writes: readonly CalendarWrite[]): Promise<void> {
|
|
42
|
-
|
|
43
|
-
(write) => write
|
|
44
|
-
);
|
|
45
|
-
await runBatch(
|
|
46
|
-
this.db,
|
|
47
|
-
writes
|
|
48
|
-
.filter((write) => write.kind !== "SettleSuggestion")
|
|
49
|
-
.flatMap((write) => this.statements(write)),
|
|
50
|
-
);
|
|
51
|
-
await runBatch(
|
|
52
|
-
this.mail,
|
|
53
|
-
settlements.flatMap((write) => this.statements(write)),
|
|
56
|
+
await this.batch(
|
|
57
|
+
writes.filter(isBatchWrite).flatMap((write) => this.statements(write)),
|
|
54
58
|
);
|
|
59
|
+
for (const write of writes.filter(isSettlement)) {
|
|
60
|
+
await this.repos.calendarSuggestion.settle(
|
|
61
|
+
write.accountConfigId,
|
|
62
|
+
write.suggestionId,
|
|
63
|
+
write.settle,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
55
66
|
}
|
|
56
67
|
|
|
57
68
|
private collection(accountConfigId: string, calendarId: string) {
|
|
@@ -97,7 +108,7 @@ export class DrizzleCalendarStore implements ICalendarStore {
|
|
|
97
108
|
);
|
|
98
109
|
}
|
|
99
110
|
|
|
100
|
-
private statements(write:
|
|
111
|
+
private statements(write: CalendarBatchWrite): SqliteBatchItem[] {
|
|
101
112
|
const now = Date.now();
|
|
102
113
|
switch (write.kind) {
|
|
103
114
|
case "PutObject": {
|
|
@@ -223,21 +234,6 @@ export class DrizzleCalendarStore implements ICalendarStore {
|
|
|
223
234
|
),
|
|
224
235
|
this.bump(write.accountConfigId, write.calendarId),
|
|
225
236
|
];
|
|
226
|
-
case "SettleSuggestion":
|
|
227
|
-
return [
|
|
228
|
-
this.mail
|
|
229
|
-
.update(calendarSuggestionTable)
|
|
230
|
-
.set({ ...write.settle, updatedAt: now })
|
|
231
|
-
.where(
|
|
232
|
-
and(
|
|
233
|
-
eq(
|
|
234
|
-
calendarSuggestionTable.accountConfigId,
|
|
235
|
-
write.accountConfigId,
|
|
236
|
-
),
|
|
237
|
-
eq(calendarSuggestionTable.suggestionId, write.suggestionId),
|
|
238
|
-
),
|
|
239
|
-
),
|
|
240
|
-
];
|
|
241
237
|
case "UpdateCollection":
|
|
242
238
|
return [
|
|
243
239
|
this.db
|
|
@@ -7,13 +7,14 @@ import type {
|
|
|
7
7
|
} from "@remit/data-ports";
|
|
8
8
|
import { deriveCalendarSuggestionId } from "@remit/data-ports/id";
|
|
9
9
|
import { CalendarSuggestionState } from "@remit/domain-enums";
|
|
10
|
-
import { and, asc, desc, eq, gte,
|
|
11
|
-
import type {
|
|
10
|
+
import { and, asc, desc, eq, gte, lt, or } from "drizzle-orm";
|
|
11
|
+
import type { RepositoryDb } from "../db.js";
|
|
12
12
|
import { NotFoundError } from "../error.js";
|
|
13
13
|
import { decodeToken, resultList } from "../pagination.js";
|
|
14
14
|
import { calendarSuggestionTable } from "../schema.js";
|
|
15
|
+
import { inList } from "./in-list.js";
|
|
15
16
|
|
|
16
|
-
type DB =
|
|
17
|
+
type DB = RepositoryDb<Record<string, unknown>>;
|
|
17
18
|
|
|
18
19
|
function rowToCalendarSuggestion(
|
|
19
20
|
row: typeof calendarSuggestionTable.$inferSelect,
|
|
@@ -111,7 +112,7 @@ export class CalendarSuggestionRepo implements ICalendarSuggestionRepository {
|
|
|
111
112
|
.where(
|
|
112
113
|
and(
|
|
113
114
|
eq(calendarSuggestionTable.accountConfigId, accountConfigId),
|
|
114
|
-
|
|
115
|
+
inList(
|
|
115
116
|
calendarSuggestionTable.acceptedCalendarObjectId,
|
|
116
117
|
calendarObjectIds,
|
|
117
118
|
),
|
|
@@ -2,6 +2,7 @@ import assert from "node:assert";
|
|
|
2
2
|
import { after, before, describe, test } from "node:test";
|
|
3
3
|
import { MailboxCursorState } from "@remit/domain-enums";
|
|
4
4
|
import { eq } from "drizzle-orm";
|
|
5
|
+
import { syncStores } from "../databases.js";
|
|
5
6
|
import {
|
|
6
7
|
accountSettingTable,
|
|
7
8
|
accountTable,
|
|
@@ -299,7 +300,7 @@ describe("runDrizzleCascadeDelete", () => {
|
|
|
299
300
|
|
|
300
301
|
test("deletes every enumerated row across all tables", async () => {
|
|
301
302
|
await runDrizzleCascadeDelete(
|
|
302
|
-
{ app: db, mail: db, search: db } as never,
|
|
303
|
+
syncStores({ app: db, mail: db, search: db } as never),
|
|
303
304
|
entities,
|
|
304
305
|
log,
|
|
305
306
|
);
|
|
@@ -438,7 +439,7 @@ describe("runDrizzleCascadeDelete", () => {
|
|
|
438
439
|
await assert.rejects(
|
|
439
440
|
() =>
|
|
440
441
|
runDrizzleCascadeDelete(
|
|
441
|
-
{ app: db, mail: db, search: db } as never,
|
|
442
|
+
syncStores({ app: db, mail: db, search: db } as never),
|
|
442
443
|
[{ entityType: "Nonsense", key: { id: "x" } }],
|
|
443
444
|
log,
|
|
444
445
|
),
|