@remit/drizzle-service 0.0.102 → 0.0.104
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 +4 -3
- 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.d1.test.ts +20 -0
- package/src/repos/account.conformance.sqlite.test.ts +6 -2
- package/src/repos/calendar-collection.conformance.d1.test.ts +20 -0
- package/src/repos/calendar-collection.ts +2 -2
- package/src/repos/calendar-default.d1.test.ts +4 -0
- package/src/repos/calendar-default.sqlite.test.ts +3 -72
- package/src/repos/calendar-default.suite.ts +68 -0
- package/src/repos/calendar-event-index.conformance.d1.test.ts +20 -0
- package/src/repos/calendar-event-index.ts +2 -2
- package/src/repos/calendar-feed-token.conformance.d1.test.ts +20 -0
- package/src/repos/calendar-feed-token.ts +2 -2
- package/src/repos/calendar-object.conformance.d1.test.ts +20 -0
- package/src/repos/calendar-object.ts +2 -2
- package/src/repos/calendar-put.d1.test.ts +4 -0
- package/src/repos/calendar-put.sqlite.test.ts +3 -205
- package/src/repos/calendar-put.suite.ts +210 -0
- package/src/repos/calendar-store.ts +36 -40
- package/src/repos/calendar-suggestion.ts +2 -2
- package/src/repos/cascade-delete.test.ts +3 -2
- package/src/repos/cascade-delete.ts +12 -8
- package/src/repos/envelope.sqlite.test.ts +3 -2
- package/src/repos/envelope.test.ts +3 -2
- package/src/repos/envelope.ts +14 -9
- package/src/repos/filter.d1.test.ts +4 -0
- package/src/repos/filter.suite.ts +476 -0
- package/src/repos/filter.test.ts +3 -472
- package/src/repos/filter.ts +2 -2
- package/src/repos/i4-account-config.d1.test.ts +4 -0
- package/src/repos/i4-account-config.suite.ts +131 -0
- package/src/repos/i4-account-config.test.ts +3 -123
- package/src/repos/i4-account-config.ts +8 -10
- package/src/repos/i4-account-export-request.d1.test.ts +4 -0
- package/src/repos/i4-account-export-request.suite.ts +44 -0
- package/src/repos/i4-account-export-request.test.ts +3 -40
- package/src/repos/i4-account-export-request.ts +2 -2
- package/src/repos/i4-account-setting.d1.test.ts +4 -0
- package/src/repos/i4-account-setting.suite.ts +100 -0
- package/src/repos/i4-account-setting.test.ts +3 -93
- package/src/repos/i4-account-setting.ts +2 -2
- package/src/repos/i4-account.test.ts +12 -8
- package/src/repos/i4-account.ts +6 -10
- package/src/repos/i4-address-junk-move.sqlite.test.ts +7 -2
- package/src/repos/i4-address.ts +12 -2
- 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 +29 -40
- 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 +13 -7
- 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 +11 -5
- package/src/repos/i4-outbox-message.ts +2 -2
- package/src/repos/i4-sender-signer-standing.ts +2 -2
- package/src/repos/label.conformance.d1.test.ts +20 -0
- package/src/repos/label.ts +2 -2
- package/src/repos/message-flag.ts +3 -6
- package/src/repos/message-label.ts +2 -2
- package/src/repos/message.sqlite.test.ts +57 -3
- package/src/repos/message.test.ts +6 -5
- package/src/repos/message.ts +81 -10
- package/src/repos/quarantine.ts +2 -2
- package/src/repos/sync-warning.ts +2 -2
- package/src/repos/thread-message-body-muted.sqlite.test.ts +30 -22
- package/src/repos/thread-message-field-terms.sqlite.test.ts +16 -11
- package/src/repos/thread-message.sqlite.test.ts +13 -7
- package/src/repos/thread-message.test.ts +43 -27
- package/src/repos/thread-message.ts +26 -14
- package/src/repos/thread-search-predicates.ts +36 -63
- package/src/schema/message-data.ts +3 -0
- package/src/schema-sets/mail.ts +1 -0
- package/src/search/thread-message-search.sqlite.test.ts +57 -11
- package/src/search/thread-message-search.ts +26 -12
- package/src/sqlite-repositories.ts +48 -32
- package/src/test-d1.ts +40 -0
- package/src/test-db-sqlite.ts +1 -1
- package/src/test-shipped-sqlite-schema.ts +8 -6
- package/src/test-stores.ts +30 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/drizzle-service",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.104",
|
|
4
4
|
"description": "Drizzle ORM service over SQLite",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -25,14 +25,15 @@
|
|
|
25
25
|
"@remit/calendar-service": "*",
|
|
26
26
|
"@remit/config-transfer": "*",
|
|
27
27
|
"drizzle-kit": "^0.31.1",
|
|
28
|
+
"miniflare": "^4.20260730.0",
|
|
28
29
|
"tsx": "*"
|
|
29
30
|
},
|
|
30
31
|
"dependencies": {
|
|
31
32
|
"@remit/data-ports": "*",
|
|
32
33
|
"@remit/domain-enums": "*",
|
|
33
34
|
"drizzle-orm": "^0.45.2",
|
|
34
|
-
"short-uuid": "
|
|
35
|
-
"uuid": "
|
|
35
|
+
"short-uuid": "^6.0.3",
|
|
36
|
+
"uuid": "^13.0.2",
|
|
36
37
|
"@remit/drizzle-sqlite-schema": "*",
|
|
37
38
|
"better-sqlite3": "^12.11.1",
|
|
38
39
|
"@types/better-sqlite3": "^7.6.13"
|
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");
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { accountRepositoryConformance } from "@remit/data-ports/conformance";
|
|
2
|
+
import { NotFoundError } from "../error.js";
|
|
3
|
+
import { randomId } from "../id.js";
|
|
4
|
+
import { composeSqliteRepositories } from "../sqlite-repositories.js";
|
|
5
|
+
import { openD1TestStores } from "../test-d1.js";
|
|
6
|
+
|
|
7
|
+
let close: (() => Promise<void>) | undefined;
|
|
8
|
+
|
|
9
|
+
accountRepositoryConformance({
|
|
10
|
+
async createRepository() {
|
|
11
|
+
const opened = await openD1TestStores();
|
|
12
|
+
close = opened.close;
|
|
13
|
+
return composeSqliteRepositories(opened.stores).account;
|
|
14
|
+
},
|
|
15
|
+
teardown: async () => {
|
|
16
|
+
await close?.();
|
|
17
|
+
},
|
|
18
|
+
makeId: () => randomId(),
|
|
19
|
+
isNotFoundError: (error) => error instanceof NotFoundError,
|
|
20
|
+
});
|
|
@@ -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?.();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { calendarCollectionRepositoryConformance } from "@remit/data-ports/conformance";
|
|
2
|
+
import { NotFoundError } from "../error.js";
|
|
3
|
+
import { randomId } from "../id.js";
|
|
4
|
+
import { openD1TestStores } from "../test-d1.js";
|
|
5
|
+
import { CalendarCollectionRepo } from "./calendar-collection.js";
|
|
6
|
+
|
|
7
|
+
let close: (() => Promise<void>) | undefined;
|
|
8
|
+
|
|
9
|
+
calendarCollectionRepositoryConformance({
|
|
10
|
+
async createRepository() {
|
|
11
|
+
const opened = await openD1TestStores();
|
|
12
|
+
close = opened.close;
|
|
13
|
+
return new CalendarCollectionRepo(opened.stores.app.db);
|
|
14
|
+
},
|
|
15
|
+
teardown: async () => {
|
|
16
|
+
await close?.();
|
|
17
|
+
},
|
|
18
|
+
makeId: () => randomId(),
|
|
19
|
+
isNotFoundError: (error) => error instanceof NotFoundError,
|
|
20
|
+
});
|
|
@@ -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,
|
|
@@ -1,73 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import { provisionDefaultCalendar } from "@remit/calendar-service";
|
|
4
|
-
import type { ICalendarStore } from "@remit/data-ports";
|
|
5
|
-
import type { Db } from "../db.js";
|
|
6
|
-
import {
|
|
7
|
-
calendarEventIndexTable,
|
|
8
|
-
calendarObjectTable,
|
|
9
|
-
calendarTable,
|
|
10
|
-
} from "../schema.js";
|
|
11
|
-
import { createSqliteTestDb } from "../test-db-sqlite.js";
|
|
12
|
-
import { CalendarCollectionRepo } from "./calendar-collection.js";
|
|
13
|
-
import { DrizzleCalendarStore } from "./calendar-store.js";
|
|
1
|
+
import { openCalendarSqliteTestStores } from "../test-stores.js";
|
|
2
|
+
import { calendarDefaultSuite } from "./calendar-default.suite.js";
|
|
14
3
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
describe("provisioning the default calendar", () => {
|
|
18
|
-
let db: Db<Record<string, unknown>>;
|
|
19
|
-
let close: () => Promise<void>;
|
|
20
|
-
let store: ICalendarStore;
|
|
21
|
-
|
|
22
|
-
before(async () => {
|
|
23
|
-
const created = await createSqliteTestDb({
|
|
24
|
-
calendars: calendarTable,
|
|
25
|
-
calendarObjects: calendarObjectTable,
|
|
26
|
-
calendarEventIndexes: calendarEventIndexTable,
|
|
27
|
-
});
|
|
28
|
-
db = created.db as unknown as Db<Record<string, unknown>>;
|
|
29
|
-
close = created.close;
|
|
30
|
-
store = new DrizzleCalendarStore(db, db);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
after(() => close());
|
|
34
|
-
|
|
35
|
-
test("leaves one calendar behind when several first reads arrive together", async () => {
|
|
36
|
-
const provisioned = await Promise.all(
|
|
37
|
-
Array.from({ length: 8 }, () =>
|
|
38
|
-
provisionDefaultCalendar(store, ACCOUNT_CONFIG_ID),
|
|
39
|
-
),
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
const ids = new Set(provisioned.map((collection) => collection.calendarId));
|
|
43
|
-
assert.equal(ids.size, 1, "every caller was handed the same calendar");
|
|
44
|
-
|
|
45
|
-
const rows = await new CalendarCollectionRepo(db).listByAccountConfig(
|
|
46
|
-
ACCOUNT_CONFIG_ID,
|
|
47
|
-
);
|
|
48
|
-
assert.equal(rows.length, 1);
|
|
49
|
-
assert.equal(rows[0]?.urlSegment, "default");
|
|
50
|
-
assert.equal(
|
|
51
|
-
rows[0]?.syncSequence,
|
|
52
|
-
0,
|
|
53
|
-
"a losing provision did not reset a collection the winner may already have written to",
|
|
54
|
-
);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("a later provision returns the stored collection rather than resetting it", async () => {
|
|
58
|
-
const repo = new CalendarCollectionRepo(db);
|
|
59
|
-
const [before] = await repo.listByAccountConfig(ACCOUNT_CONFIG_ID);
|
|
60
|
-
assert.ok(before);
|
|
61
|
-
await repo.bumpSyncSequence(ACCOUNT_CONFIG_ID, before.calendarId);
|
|
62
|
-
|
|
63
|
-
const again = await provisionDefaultCalendar(
|
|
64
|
-
store,
|
|
65
|
-
ACCOUNT_CONFIG_ID,
|
|
66
|
-
"A different name",
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
assert.equal(again.calendarId, before.calendarId);
|
|
70
|
-
assert.equal(again.displayName, before.displayName);
|
|
71
|
-
assert.equal(again.syncSequence, 1);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
4
|
+
calendarDefaultSuite(openCalendarSqliteTestStores);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { after, before, describe, test } from "node:test";
|
|
3
|
+
import { provisionDefaultCalendar } from "@remit/calendar-service";
|
|
4
|
+
import type { ICalendarStore } from "@remit/data-ports";
|
|
5
|
+
import type { RepositoryDb } from "../db.js";
|
|
6
|
+
import { composeSqliteRepositories } from "../sqlite-repositories.js";
|
|
7
|
+
import type { OpenTestStores } from "../test-stores.js";
|
|
8
|
+
import { CalendarCollectionRepo } from "./calendar-collection.js";
|
|
9
|
+
|
|
10
|
+
const ACCOUNT_CONFIG_ID = "account-config-default";
|
|
11
|
+
|
|
12
|
+
export const calendarDefaultSuite = (open: OpenTestStores): void => {
|
|
13
|
+
describe("provisioning the default calendar", () => {
|
|
14
|
+
let db: RepositoryDb<Record<string, unknown>>;
|
|
15
|
+
let close: () => Promise<void>;
|
|
16
|
+
let store: ICalendarStore;
|
|
17
|
+
|
|
18
|
+
before(async () => {
|
|
19
|
+
const opened = await open();
|
|
20
|
+
db = opened.stores.app.db;
|
|
21
|
+
close = opened.close;
|
|
22
|
+
store = composeSqliteRepositories(opened.stores).calendarStore;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
after(() => close());
|
|
26
|
+
|
|
27
|
+
test("leaves one calendar behind when several first reads arrive together", async () => {
|
|
28
|
+
const provisioned = await Promise.all(
|
|
29
|
+
Array.from({ length: 8 }, () =>
|
|
30
|
+
provisionDefaultCalendar(store, ACCOUNT_CONFIG_ID),
|
|
31
|
+
),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const ids = new Set(
|
|
35
|
+
provisioned.map((collection) => collection.calendarId),
|
|
36
|
+
);
|
|
37
|
+
assert.equal(ids.size, 1, "every caller was handed the same calendar");
|
|
38
|
+
|
|
39
|
+
const rows = await new CalendarCollectionRepo(db).listByAccountConfig(
|
|
40
|
+
ACCOUNT_CONFIG_ID,
|
|
41
|
+
);
|
|
42
|
+
assert.equal(rows.length, 1);
|
|
43
|
+
assert.equal(rows[0]?.urlSegment, "default");
|
|
44
|
+
assert.equal(
|
|
45
|
+
rows[0]?.syncSequence,
|
|
46
|
+
0,
|
|
47
|
+
"a losing provision did not reset a collection the winner may already have written to",
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("a later provision returns the stored collection rather than resetting it", async () => {
|
|
52
|
+
const repo = new CalendarCollectionRepo(db);
|
|
53
|
+
const [before] = await repo.listByAccountConfig(ACCOUNT_CONFIG_ID);
|
|
54
|
+
assert.ok(before);
|
|
55
|
+
await repo.bumpSyncSequence(ACCOUNT_CONFIG_ID, before.calendarId);
|
|
56
|
+
|
|
57
|
+
const again = await provisionDefaultCalendar(
|
|
58
|
+
store,
|
|
59
|
+
ACCOUNT_CONFIG_ID,
|
|
60
|
+
"A different name",
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
assert.equal(again.calendarId, before.calendarId);
|
|
64
|
+
assert.equal(again.displayName, before.displayName);
|
|
65
|
+
assert.equal(again.syncSequence, 1);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { calendarEventIndexRepositoryConformance } from "@remit/data-ports/conformance";
|
|
2
|
+
import { NotFoundError } from "../error.js";
|
|
3
|
+
import { randomId } from "../id.js";
|
|
4
|
+
import { openD1TestStores } from "../test-d1.js";
|
|
5
|
+
import { CalendarEventIndexRepo } from "./calendar-event-index.js";
|
|
6
|
+
|
|
7
|
+
let close: (() => Promise<void>) | undefined;
|
|
8
|
+
|
|
9
|
+
calendarEventIndexRepositoryConformance({
|
|
10
|
+
async createRepository() {
|
|
11
|
+
const opened = await openD1TestStores();
|
|
12
|
+
close = opened.close;
|
|
13
|
+
return new CalendarEventIndexRepo(opened.stores.app.db);
|
|
14
|
+
},
|
|
15
|
+
teardown: async () => {
|
|
16
|
+
await close?.();
|
|
17
|
+
},
|
|
18
|
+
makeId: () => randomId(),
|
|
19
|
+
isNotFoundError: (error) => error instanceof NotFoundError,
|
|
20
|
+
});
|
|
@@ -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,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { calendarFeedTokenRepositoryConformance } from "@remit/data-ports/conformance";
|
|
2
|
+
import { NotFoundError } from "../error.js";
|
|
3
|
+
import { randomId } from "../id.js";
|
|
4
|
+
import { openD1TestStores } from "../test-d1.js";
|
|
5
|
+
import { CalendarFeedTokenRepo } from "./calendar-feed-token.js";
|
|
6
|
+
|
|
7
|
+
let close: (() => Promise<void>) | undefined;
|
|
8
|
+
|
|
9
|
+
calendarFeedTokenRepositoryConformance({
|
|
10
|
+
async createRepository() {
|
|
11
|
+
const opened = await openD1TestStores();
|
|
12
|
+
close = opened.close;
|
|
13
|
+
return new CalendarFeedTokenRepo(opened.stores.app.db);
|
|
14
|
+
},
|
|
15
|
+
teardown: async () => {
|
|
16
|
+
await close?.();
|
|
17
|
+
},
|
|
18
|
+
makeId: () => randomId(),
|
|
19
|
+
isNotFoundError: (error) => error instanceof NotFoundError,
|
|
20
|
+
});
|
|
@@ -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,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { calendarObjectRepositoryConformance } from "@remit/data-ports/conformance";
|
|
2
|
+
import { NotFoundError } from "../error.js";
|
|
3
|
+
import { randomId } from "../id.js";
|
|
4
|
+
import { openD1TestStores } from "../test-d1.js";
|
|
5
|
+
import { CalendarObjectRepo } from "./calendar-object.js";
|
|
6
|
+
|
|
7
|
+
let close: (() => Promise<void>) | undefined;
|
|
8
|
+
|
|
9
|
+
calendarObjectRepositoryConformance({
|
|
10
|
+
async createRepository() {
|
|
11
|
+
const opened = await openD1TestStores();
|
|
12
|
+
close = opened.close;
|
|
13
|
+
return new CalendarObjectRepo(opened.stores.app.db);
|
|
14
|
+
},
|
|
15
|
+
teardown: async () => {
|
|
16
|
+
await close?.();
|
|
17
|
+
},
|
|
18
|
+
makeId: () => randomId(),
|
|
19
|
+
isNotFoundError: (error) => error instanceof NotFoundError,
|
|
20
|
+
});
|
|
@@ -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,
|