@remit/drizzle-service 0.0.68 → 0.0.69

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/drizzle-service",
3
- "version": "0.0.68",
3
+ "version": "0.0.69",
4
4
  "description": "Drizzle ORM service over SQLite",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@ export { CreateFailedConflictError, NotFoundError } from "./error.js";
2
2
  export { CalendarCollectionRepo } from "./repos/calendar-collection.js";
3
3
  export { CalendarEventIndexRepo } from "./repos/calendar-event-index.js";
4
4
  export { CalendarObjectRepo } from "./repos/calendar-object.js";
5
+ export { CalendarSuggestionRepo } from "./repos/calendar-suggestion.js";
5
6
  export { DrizzleCalendarUnitOfWork } from "./repos/calendar-unit-of-work.js";
6
7
  export {
7
8
  type CascadeDeleteLogger,
@@ -0,0 +1,23 @@
1
+ import { calendarSuggestionRepositoryConformance } from "@remit/data-ports/conformance";
2
+ import { NotFoundError } from "../error.js";
3
+ import { randomId } from "../id.js";
4
+ import { calendarSuggestionTable } from "../schema.js";
5
+ import { createSqliteTestDb } from "../test-db-sqlite.js";
6
+ import { CalendarSuggestionRepo } from "./calendar-suggestion.js";
7
+
8
+ let close: (() => Promise<void>) | undefined;
9
+
10
+ calendarSuggestionRepositoryConformance({
11
+ async createRepository() {
12
+ const { db, close: closeDb } = await createSqliteTestDb({
13
+ calendarSuggestions: calendarSuggestionTable,
14
+ });
15
+ close = closeDb;
16
+ return new CalendarSuggestionRepo(db as never);
17
+ },
18
+ teardown: async () => {
19
+ await close?.();
20
+ },
21
+ makeId: () => randomId(),
22
+ isNotFoundError: (error) => error instanceof NotFoundError,
23
+ });
@@ -0,0 +1,223 @@
1
+ import type {
2
+ CalendarSuggestionItem,
3
+ ICalendarSuggestionRepository,
4
+ PutCalendarSuggestionInput,
5
+ ResultList,
6
+ SettleCalendarSuggestionInput,
7
+ } from "@remit/data-ports";
8
+ import { deriveCalendarSuggestionId } from "@remit/data-ports/id";
9
+ import { CalendarSuggestionState } from "@remit/domain-enums";
10
+ import { and, asc, desc, eq, lt, or } from "drizzle-orm";
11
+ import type { Db } from "../db.js";
12
+ import { NotFoundError } from "../error.js";
13
+ import { decodeToken, resultList } from "../pagination.js";
14
+ import { calendarSuggestionTable } from "../schema.js";
15
+
16
+ type DB = Db<Record<string, unknown>>;
17
+
18
+ function rowToCalendarSuggestion(
19
+ row: typeof calendarSuggestionTable.$inferSelect,
20
+ ): CalendarSuggestionItem {
21
+ return {
22
+ suggestionId: row.suggestionId,
23
+ accountConfigId: row.accountConfigId,
24
+ messageId: row.messageId,
25
+ bodyPartId: row.bodyPartId,
26
+ icalUid: row.icalUid,
27
+ sequence: row.sequence,
28
+ method: row.method,
29
+ source: row.source,
30
+ state: row.state,
31
+ summary: row.summary,
32
+ dtStart: row.dtStart,
33
+ dtEnd: row.dtEnd,
34
+ allDay: row.allDay,
35
+ location: row.location,
36
+ organizer: row.organizer,
37
+ zoneCertainty: row.zoneCertainty,
38
+ icalData: row.icalData,
39
+ acceptedCalendarObjectId: row.acceptedCalendarObjectId,
40
+ createdAt: row.createdAt,
41
+ updatedAt: row.updatedAt,
42
+ };
43
+ }
44
+
45
+ export class CalendarSuggestionRepo implements ICalendarSuggestionRepository {
46
+ constructor(private db: DB) {}
47
+
48
+ async put(
49
+ input: PutCalendarSuggestionInput,
50
+ ): Promise<CalendarSuggestionItem> {
51
+ const now = Date.now();
52
+ const suggestionId = deriveCalendarSuggestionId(
53
+ input.messageId,
54
+ input.bodyPartId,
55
+ input.icalUid,
56
+ );
57
+ const [row] = await this.db
58
+ .insert(calendarSuggestionTable)
59
+ .values({
60
+ ...input,
61
+ suggestionId,
62
+ state: CalendarSuggestionState.Pending,
63
+ acceptedCalendarObjectId: "",
64
+ createdAt: now,
65
+ updatedAt: now,
66
+ })
67
+ .onConflictDoUpdate({
68
+ target: [
69
+ calendarSuggestionTable.accountConfigId,
70
+ calendarSuggestionTable.suggestionId,
71
+ ],
72
+ // `state` and `acceptedCalendarObjectId` are deliberately absent from
73
+ // the update set: they carry what a person decided, and a producer
74
+ // re-reading the message must not walk that back to Pending.
75
+ set: { ...input, updatedAt: now },
76
+ })
77
+ .returning();
78
+ return rowToCalendarSuggestion(row);
79
+ }
80
+
81
+ async get(
82
+ accountConfigId: string,
83
+ suggestionId: string,
84
+ ): Promise<CalendarSuggestionItem> {
85
+ const [row] = await this.db
86
+ .select()
87
+ .from(calendarSuggestionTable)
88
+ .where(
89
+ and(
90
+ eq(calendarSuggestionTable.accountConfigId, accountConfigId),
91
+ eq(calendarSuggestionTable.suggestionId, suggestionId),
92
+ ),
93
+ );
94
+ if (!row) {
95
+ throw new NotFoundError(`Calendar suggestion not found: ${suggestionId}`);
96
+ }
97
+ return rowToCalendarSuggestion(row);
98
+ }
99
+
100
+ async listByMessage(
101
+ accountConfigId: string,
102
+ messageId: string,
103
+ ): Promise<CalendarSuggestionItem[]> {
104
+ const rows = await this.db
105
+ .select()
106
+ .from(calendarSuggestionTable)
107
+ .where(
108
+ and(
109
+ eq(calendarSuggestionTable.accountConfigId, accountConfigId),
110
+ eq(calendarSuggestionTable.messageId, messageId),
111
+ ),
112
+ )
113
+ .orderBy(
114
+ asc(calendarSuggestionTable.createdAt),
115
+ asc(calendarSuggestionTable.suggestionId),
116
+ );
117
+ return rows.map(rowToCalendarSuggestion);
118
+ }
119
+
120
+ /**
121
+ * Newest first, paged on a keyset over `(createdAt, suggestionId)` — the
122
+ * exact trailing members of the `byState` index's sort key, read backwards.
123
+ * The order is the index's own, never a sort applied on top of it.
124
+ */
125
+ async listByState(
126
+ accountConfigId: string,
127
+ state: CalendarSuggestionItem["state"],
128
+ options?: { limit?: number; continuationToken?: string },
129
+ ): Promise<ResultList<CalendarSuggestionItem>> {
130
+ const limit = options?.limit ?? 100;
131
+ const cursor = options?.continuationToken
132
+ ? decodeToken(options.continuationToken)
133
+ : undefined;
134
+ const after = cursor
135
+ ? {
136
+ createdAt: cursor.createdAt as number,
137
+ suggestionId: cursor.suggestionId as string,
138
+ }
139
+ : undefined;
140
+
141
+ const rows = await this.db
142
+ .select()
143
+ .from(calendarSuggestionTable)
144
+ .where(
145
+ and(
146
+ eq(calendarSuggestionTable.accountConfigId, accountConfigId),
147
+ eq(calendarSuggestionTable.state, state),
148
+ after
149
+ ? or(
150
+ lt(calendarSuggestionTable.createdAt, after.createdAt),
151
+ and(
152
+ eq(calendarSuggestionTable.createdAt, after.createdAt),
153
+ lt(calendarSuggestionTable.suggestionId, after.suggestionId),
154
+ ),
155
+ )
156
+ : undefined,
157
+ ),
158
+ )
159
+ .orderBy(
160
+ desc(calendarSuggestionTable.createdAt),
161
+ desc(calendarSuggestionTable.suggestionId),
162
+ )
163
+ .limit(limit);
164
+
165
+ const items = rows.map(rowToCalendarSuggestion);
166
+ const lastItem = items[items.length - 1];
167
+ return resultList(
168
+ items,
169
+ limit,
170
+ lastItem
171
+ ? {
172
+ createdAt: lastItem.createdAt,
173
+ suggestionId: lastItem.suggestionId,
174
+ }
175
+ : undefined,
176
+ );
177
+ }
178
+
179
+ async settle(
180
+ accountConfigId: string,
181
+ suggestionId: string,
182
+ input: SettleCalendarSuggestionInput,
183
+ ): Promise<CalendarSuggestionItem> {
184
+ const [row] = await this.db
185
+ .update(calendarSuggestionTable)
186
+ .set({ ...input, updatedAt: Date.now() })
187
+ .where(
188
+ and(
189
+ eq(calendarSuggestionTable.accountConfigId, accountConfigId),
190
+ eq(calendarSuggestionTable.suggestionId, suggestionId),
191
+ ),
192
+ )
193
+ .returning();
194
+ if (!row) {
195
+ throw new NotFoundError(`Calendar suggestion not found: ${suggestionId}`);
196
+ }
197
+ return rowToCalendarSuggestion(row);
198
+ }
199
+
200
+ async supersedeIfPending(
201
+ accountConfigId: string,
202
+ suggestionId: string,
203
+ ): Promise<CalendarSuggestionItem | null> {
204
+ const [row] = await this.db
205
+ .update(calendarSuggestionTable)
206
+ .set({
207
+ state: CalendarSuggestionState.Superseded,
208
+ acceptedCalendarObjectId: "",
209
+ updatedAt: Date.now(),
210
+ })
211
+ .where(
212
+ and(
213
+ eq(calendarSuggestionTable.accountConfigId, accountConfigId),
214
+ eq(calendarSuggestionTable.suggestionId, suggestionId),
215
+ // The condition is the point: a card the user answered between
216
+ // the producer's read and this write must keep their answer.
217
+ eq(calendarSuggestionTable.state, CalendarSuggestionState.Pending),
218
+ ),
219
+ )
220
+ .returning();
221
+ return row ? rowToCalendarSuggestion(row) : null;
222
+ }
223
+ }
@@ -7,6 +7,7 @@ import { runInTransaction } from "../tx.js";
7
7
  import { CalendarCollectionRepo } from "./calendar-collection.js";
8
8
  import { CalendarEventIndexRepo } from "./calendar-event-index.js";
9
9
  import { CalendarObjectRepo } from "./calendar-object.js";
10
+ import { CalendarSuggestionRepo } from "./calendar-suggestion.js";
10
11
 
11
12
  type DB = Db<Record<string, unknown>>;
12
13
 
@@ -21,6 +22,7 @@ export class DrizzleCalendarUnitOfWork implements ICalendarUnitOfWork {
21
22
  calendarCollection: new CalendarCollectionRepo(tx as never),
22
23
  calendarObject: new CalendarObjectRepo(tx as never),
23
24
  calendarEventIndex: new CalendarEventIndexRepo(tx as never),
25
+ calendarSuggestion: new CalendarSuggestionRepo(tx as never),
24
26
  }),
25
27
  );
26
28
  }
package/src/schema.ts CHANGED
@@ -11,6 +11,7 @@ import * as entities from "@remit/drizzle-sqlite-schema";
11
11
  export const calendarTable = entities.calendarCollections;
12
12
  export const calendarObjectTable = entities.calendarObjects;
13
13
  export const calendarEventIndexTable = entities.calendarEventIndexes;
14
+ export const calendarSuggestionTable = entities.calendarSuggestions;
14
15
  export const filterAnchorTable = entities.filterAnchors;
15
16
  export const filterTable = entities.filters;
16
17
  export const labelTable = entities.labels;