@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.
Files changed (88) hide show
  1. package/package.json +4 -3
  2. package/src/batch.sqlite.test.ts +4 -4
  3. package/src/batch.ts +47 -21
  4. package/src/databases.ts +24 -6
  5. package/src/db.ts +20 -5
  6. package/src/index.ts +12 -0
  7. package/src/repair/junk-only-address.sqlite.test.ts +7 -2
  8. package/src/repos/account.conformance.d1.test.ts +20 -0
  9. package/src/repos/account.conformance.sqlite.test.ts +6 -2
  10. package/src/repos/calendar-collection.conformance.d1.test.ts +20 -0
  11. package/src/repos/calendar-collection.ts +2 -2
  12. package/src/repos/calendar-default.d1.test.ts +4 -0
  13. package/src/repos/calendar-default.sqlite.test.ts +3 -72
  14. package/src/repos/calendar-default.suite.ts +68 -0
  15. package/src/repos/calendar-event-index.conformance.d1.test.ts +20 -0
  16. package/src/repos/calendar-event-index.ts +2 -2
  17. package/src/repos/calendar-feed-token.conformance.d1.test.ts +20 -0
  18. package/src/repos/calendar-feed-token.ts +2 -2
  19. package/src/repos/calendar-object.conformance.d1.test.ts +20 -0
  20. package/src/repos/calendar-object.ts +2 -2
  21. package/src/repos/calendar-put.d1.test.ts +4 -0
  22. package/src/repos/calendar-put.sqlite.test.ts +3 -205
  23. package/src/repos/calendar-put.suite.ts +210 -0
  24. package/src/repos/calendar-store.ts +36 -40
  25. package/src/repos/calendar-suggestion.ts +2 -2
  26. package/src/repos/cascade-delete.test.ts +3 -2
  27. package/src/repos/cascade-delete.ts +12 -8
  28. package/src/repos/envelope.sqlite.test.ts +3 -2
  29. package/src/repos/envelope.test.ts +3 -2
  30. package/src/repos/envelope.ts +14 -9
  31. package/src/repos/filter.d1.test.ts +4 -0
  32. package/src/repos/filter.suite.ts +476 -0
  33. package/src/repos/filter.test.ts +3 -472
  34. package/src/repos/filter.ts +2 -2
  35. package/src/repos/i4-account-config.d1.test.ts +4 -0
  36. package/src/repos/i4-account-config.suite.ts +131 -0
  37. package/src/repos/i4-account-config.test.ts +3 -123
  38. package/src/repos/i4-account-config.ts +8 -10
  39. package/src/repos/i4-account-export-request.d1.test.ts +4 -0
  40. package/src/repos/i4-account-export-request.suite.ts +44 -0
  41. package/src/repos/i4-account-export-request.test.ts +3 -40
  42. package/src/repos/i4-account-export-request.ts +2 -2
  43. package/src/repos/i4-account-setting.d1.test.ts +4 -0
  44. package/src/repos/i4-account-setting.suite.ts +100 -0
  45. package/src/repos/i4-account-setting.test.ts +3 -93
  46. package/src/repos/i4-account-setting.ts +2 -2
  47. package/src/repos/i4-account.test.ts +12 -8
  48. package/src/repos/i4-account.ts +6 -10
  49. package/src/repos/i4-address-junk-move.sqlite.test.ts +7 -2
  50. package/src/repos/i4-address.ts +12 -2
  51. package/src/repos/i4-config-import.ts +2 -2
  52. package/src/repos/i4-mailbox-lock.ts +2 -2
  53. package/src/repos/i4-mailbox-special-use.sqlite.test.ts +12 -10
  54. package/src/repos/i4-mailbox-special-use.ts +29 -40
  55. package/src/repos/i4-mailbox.sqlite.test.ts +12 -10
  56. package/src/repos/i4-mailbox.test.ts +15 -14
  57. package/src/repos/i4-mailbox.ts +13 -7
  58. package/src/repos/i4-message-flag-push.ts +2 -2
  59. package/src/repos/i4-message-placement-move.ts +2 -2
  60. package/src/repos/i4-organize-job-request.ts +2 -2
  61. package/src/repos/i4-outbox-attachment.test.ts +7 -2
  62. package/src/repos/i4-outbox-attachment.ts +11 -5
  63. package/src/repos/i4-outbox-message.ts +2 -2
  64. package/src/repos/i4-sender-signer-standing.ts +2 -2
  65. package/src/repos/label.conformance.d1.test.ts +20 -0
  66. package/src/repos/label.ts +2 -2
  67. package/src/repos/message-flag.ts +3 -6
  68. package/src/repos/message-label.ts +2 -2
  69. package/src/repos/message.sqlite.test.ts +57 -3
  70. package/src/repos/message.test.ts +6 -5
  71. package/src/repos/message.ts +81 -10
  72. package/src/repos/quarantine.ts +2 -2
  73. package/src/repos/sync-warning.ts +2 -2
  74. package/src/repos/thread-message-body-muted.sqlite.test.ts +30 -22
  75. package/src/repos/thread-message-field-terms.sqlite.test.ts +16 -11
  76. package/src/repos/thread-message.sqlite.test.ts +13 -7
  77. package/src/repos/thread-message.test.ts +43 -27
  78. package/src/repos/thread-message.ts +26 -14
  79. package/src/repos/thread-search-predicates.ts +36 -63
  80. package/src/schema/message-data.ts +3 -0
  81. package/src/schema-sets/mail.ts +1 -0
  82. package/src/search/thread-message-search.sqlite.test.ts +57 -11
  83. package/src/search/thread-message-search.ts +26 -12
  84. package/src/sqlite-repositories.ts +48 -32
  85. package/src/test-d1.ts +40 -0
  86. package/src/test-db-sqlite.ts +1 -1
  87. package/src/test-shipped-sqlite-schema.ts +8 -6
  88. 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.102",
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"
@@ -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 { runBatch } from "./batch.js";
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 runBatch(db, [
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
- runBatch(db, [
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 runBatch(db, []), []);
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
- * The shape of D1's `batch()`: every statement commits together or not at all,
24
- * and none of them reads another's result mid-flight. A write that needs a
25
- * value reads it before the batch and is written so a stale read loses safely
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 runBatch = async <
35
- TSchema extends Record<string, unknown>,
36
- T extends readonly SqliteBatchItem[],
37
- >(
38
- db: Db<TSchema>,
39
- items: T,
40
- ): Promise<BatchResponse<T>> => {
41
- if (items.length === 0) return [] as unknown as BatchResponse<T>;
42
- const results = db.transaction(() => items.map(runSync), {
43
- behavior: "immediate",
44
- });
45
- return results as unknown as BatchResponse<T>;
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 type { Db } from "./db.js";
2
- import {
3
- type MessageDataSchema,
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<MessageDataSchema>;
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
- * The drizzle handle a repository runs on: any synchronous SQLite driver.
5
- *
6
- * better-sqlite3 is the one this package opens; a Cloudflare Durable Object's
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 = new MailboxSpecialUseRepo(db as never, db as never);
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 new AccountRepo(db as never, db as never);
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 { Db } from "../db.js";
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 = Db<Record<string, unknown>>;
16
+ type DB = RepositoryDb<Record<string, unknown>>;
17
17
 
18
18
  function rowToCalendar(
19
19
  row: typeof calendarTable.$inferSelect,
@@ -0,0 +1,4 @@
1
+ import { openD1TestStores } from "../test-d1.js";
2
+ import { calendarDefaultSuite } from "./calendar-default.suite.js";
3
+
4
+ calendarDefaultSuite(openD1TestStores);
@@ -1,73 +1,4 @@
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 { 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
- const ACCOUNT_CONFIG_ID = "account-config-default";
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 { Db } from "../db.js";
7
+ import type { RepositoryDb } from "../db.js";
8
8
  import { calendarEventIndexTable } from "../schema.js";
9
9
 
10
- type DB = Db<Record<string, unknown>>;
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 { Db } from "../db.js";
8
+ import type { RepositoryDb } from "../db.js";
9
9
  import { calendarFeedTokenTable } from "../schema.js";
10
10
 
11
- type DB = Db<Record<string, unknown>>;
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 { Db } from "../db.js";
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 = Db<Record<string, unknown>>;
12
+ type DB = RepositoryDb<Record<string, unknown>>;
13
13
 
14
14
  function rowToCalendarObject(
15
15
  row: typeof calendarObjectTable.$inferSelect,
@@ -0,0 +1,4 @@
1
+ import { openD1TestStores } from "../test-d1.js";
2
+ import { calendarPutSuite } from "./calendar-put.suite.js";
3
+
4
+ calendarPutSuite(openD1TestStores);