@wirecat/cli-core 0.454.0 → 0.456.0
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/CHANGELOG.md +59 -0
- package/dist/calendar/cli/index.d.ts +24 -0
- package/dist/calendar/cli/index.js +173 -0
- package/dist/calendar/index.d.ts +3 -0
- package/dist/calendar/index.js +1 -0
- package/dist/calendar/mcp/index.d.ts +307 -0
- package/dist/calendar/mcp/index.js +132 -0
- package/dist/calendar/reads.d.ts +149 -0
- package/dist/calendar/reads.js +214 -0
- package/dist/calendar/source.d.ts +54 -0
- package/dist/calendar/source.js +1 -0
- package/dist/calendar/sync.d.ts +65 -0
- package/dist/calendar/sync.js +167 -0
- package/dist/calendar/testing/index.d.ts +5 -0
- package/dist/calendar/testing/index.js +5 -0
- package/dist/calendar/testing/memory.d.ts +6 -0
- package/dist/calendar/testing/memory.js +478 -0
- package/dist/calendar/testing/source-contract.d.ts +31 -0
- package/dist/calendar/testing/source-contract.js +171 -0
- package/dist/calendar/testing/source.d.ts +38 -0
- package/dist/calendar/testing/source.js +110 -0
- package/dist/calendar/testing/sync-contract.d.ts +15 -0
- package/dist/calendar/testing/sync-contract.js +211 -0
- package/dist/cli/index.d.ts +2 -1
- package/dist/cli/index.js +2 -1
- package/dist/cli/messenger/ask.js +1 -1
- package/dist/errors/reasons.js +1 -0
- package/dist/meetings/calendar-text.d.ts +1 -1
- package/dist/meetings/calendar-text.js +0 -1
- package/dist/meetings/event-store.d.ts +42 -15
- package/dist/meetings/index.d.ts +1 -1
- package/dist/meetings/testing/event-store-contract.js +83 -4
- package/dist/sends/guard.d.ts +2 -0
- package/dist/sends/guard.js +1 -0
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.js +1 -1
- package/dist/services/people.d.ts +6 -1
- package/dist/services/people.js +14 -11
- package/dist/store/sqlite/calendar.js +68 -31
- package/dist/store/sqlite/manifest.js +2 -0
- package/dist/store/sqlite/migrations.generated.js +17 -0
- package/dist/store/sqlite/note-index.d.ts +1 -1
- package/dist/store/sqlite/note-index.js +6 -1
- package/dist/store/sqlite/schema.d.ts +16 -0
- package/dist/store/sqlite/schema.js +4 -0
- package/package.json +17 -1
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import type { Event } from "./model.js";
|
|
2
|
+
/** RFC 5545 PARTSTAT: a participant's answer, and the owner's own. */
|
|
3
|
+
export type ParticipantResponse = "needs-action" | "accepted" | "tentative" | "declined" | "delegated";
|
|
4
|
+
/** What kind of entry it is. Only `default` and `from-mail` are searched (requirements 3.4). */
|
|
5
|
+
export type EntryEventType = "default" | "out-of-office" | "focus" | "working-location" | "birthday" | "from-mail";
|
|
6
|
+
/** RFC 5545 TRANSP. */
|
|
7
|
+
export type EntryTransparency = "opaque" | "transparent";
|
|
8
|
+
/** RFC 5545 CLASS, plus `default`: the calendar's own setting. */
|
|
9
|
+
export type EntryVisibility = "default" | "public" | "private" | "confidential";
|
|
10
|
+
/** What the owner may do with a calendar: `free-busy` sees only busy slots. */
|
|
11
|
+
export type CalendarAccessRole = "owner" | "writer" | "reader" | "free-busy";
|
|
2
12
|
/** One calendar as a provider lists it. */
|
|
3
13
|
export interface CalendarInput {
|
|
4
14
|
externalId: string;
|
|
5
15
|
title: string | null;
|
|
6
16
|
timezone: string | null;
|
|
7
|
-
|
|
8
|
-
accessRole: string | null;
|
|
17
|
+
accessRole: CalendarAccessRole | null;
|
|
9
18
|
primary: boolean;
|
|
19
|
+
/** Whether a calendar seen for the first time syncs; the adapter decides. Later lists leave `synced` alone. */
|
|
20
|
+
syncByDefault: boolean;
|
|
10
21
|
}
|
|
11
22
|
export interface StoredCalendar {
|
|
12
23
|
id: number;
|
|
@@ -14,7 +25,7 @@ export interface StoredCalendar {
|
|
|
14
25
|
externalId: string;
|
|
15
26
|
title: string | null;
|
|
16
27
|
timezone: string | null;
|
|
17
|
-
accessRole:
|
|
28
|
+
accessRole: CalendarAccessRole | null;
|
|
18
29
|
primary: boolean;
|
|
19
30
|
synced: boolean;
|
|
20
31
|
seenAt: number | null;
|
|
@@ -23,7 +34,7 @@ export interface StoredCalendar {
|
|
|
23
34
|
export interface EntryParticipantInput {
|
|
24
35
|
email: string;
|
|
25
36
|
name?: string | null;
|
|
26
|
-
response?:
|
|
37
|
+
response?: ParticipantResponse | null;
|
|
27
38
|
optional?: boolean;
|
|
28
39
|
organizer?: boolean;
|
|
29
40
|
self?: boolean;
|
|
@@ -53,10 +64,11 @@ export type EntryStatus = "confirmed" | "tentative" | "cancelled";
|
|
|
53
64
|
export interface EntryInput {
|
|
54
65
|
externalId: string;
|
|
55
66
|
kind: EntryKind;
|
|
56
|
-
/** An occurrence's series, by the series entry's `externalId
|
|
67
|
+
/** An occurrence's series, by the series entry's `externalId`. */
|
|
57
68
|
seriesExternalId?: string | null;
|
|
69
|
+
/** The RFC 5545 UID of the series: the same on the series and every occurrence, and across calendars. */
|
|
58
70
|
icalUid?: string | null;
|
|
59
|
-
/** An occurrence's slot in its series
|
|
71
|
+
/** An occurrence's slot in its series, as first scheduled; an all-day slot is midnight UTC of its date. */
|
|
60
72
|
originalStartsAt?: number | null;
|
|
61
73
|
exception?: boolean;
|
|
62
74
|
status: EntryStatus;
|
|
@@ -72,10 +84,10 @@ export interface EntryInput {
|
|
|
72
84
|
recurrence?: string[] | null;
|
|
73
85
|
/** A series' last slot; `null` repeats indefinitely. */
|
|
74
86
|
recurrenceEndsAt?: number | null;
|
|
75
|
-
eventType?:
|
|
76
|
-
transparency?:
|
|
77
|
-
visibility?:
|
|
78
|
-
ownResponse?:
|
|
87
|
+
eventType?: EntryEventType | null;
|
|
88
|
+
transparency?: EntryTransparency | null;
|
|
89
|
+
visibility?: EntryVisibility | null;
|
|
90
|
+
ownResponse?: ParticipantResponse | null;
|
|
79
91
|
organizer?: {
|
|
80
92
|
email: string;
|
|
81
93
|
name?: string | null;
|
|
@@ -101,6 +113,8 @@ export interface StoredEntry {
|
|
|
101
113
|
calendarId: number;
|
|
102
114
|
kind: EntryKind;
|
|
103
115
|
seriesEntryId: number | null;
|
|
116
|
+
/** On an occurrence: its series' `externalId`, also before that series is stored. */
|
|
117
|
+
seriesExternalId: string | null;
|
|
104
118
|
eventId: number | null;
|
|
105
119
|
eventSeriesId: number | null;
|
|
106
120
|
externalId: string;
|
|
@@ -119,8 +133,8 @@ export interface StoredEntry {
|
|
|
119
133
|
timezone: string | null;
|
|
120
134
|
recurrence: string[] | null;
|
|
121
135
|
recurrenceEndsAt: number | null;
|
|
122
|
-
eventType:
|
|
123
|
-
ownResponse:
|
|
136
|
+
eventType: EntryEventType | null;
|
|
137
|
+
ownResponse: ParticipantResponse | null;
|
|
124
138
|
organizerIdentityId: number | null;
|
|
125
139
|
joinUrl: string | null;
|
|
126
140
|
joinKey: string | null;
|
|
@@ -134,7 +148,7 @@ export interface StoredParticipant {
|
|
|
134
148
|
identityId: number | null;
|
|
135
149
|
email: string;
|
|
136
150
|
name: string | null;
|
|
137
|
-
response:
|
|
151
|
+
response: ParticipantResponse | null;
|
|
138
152
|
optional: boolean;
|
|
139
153
|
organizer: boolean;
|
|
140
154
|
self: boolean;
|
|
@@ -205,13 +219,26 @@ export interface EventStore {
|
|
|
205
219
|
/** One sync page, matched by rules 1–5, in one transaction. */
|
|
206
220
|
saveEntries(calendarId: number, entries: EntryInput[], now: number): Promise<StoredEntry[]>;
|
|
207
221
|
/**
|
|
208
|
-
*
|
|
209
|
-
*
|
|
222
|
+
* Entries the provider confirmed deleted and names only by id, cancelled as a cancelled entry would be: past events
|
|
223
|
+
* stay as history, future ones cancel, a series cancels its occurrences. Unknown ids are skipped; the count is of
|
|
224
|
+
* entries cancelled now.
|
|
225
|
+
*/
|
|
226
|
+
removeEntries(calendarId: number, externalIds: string[], now: number): Promise<number>;
|
|
227
|
+
/**
|
|
228
|
+
* After a full list: entries not seen since `seenBefore` get `deleted_at`. Plain occurrences come only from the
|
|
229
|
+
* window fetched, so only those inside it count. Events, series and links stay.
|
|
210
230
|
*/
|
|
211
231
|
markUnseen(calendarId: number, seenBefore: number, window: {
|
|
212
232
|
from: number;
|
|
213
233
|
to: number;
|
|
214
234
|
}, now: number): Promise<number>;
|
|
235
|
+
/**
|
|
236
|
+
* Where the calendar's last sync stopped, as the sync engine wrote it (`{"v":1,"token":…,"window":…,"fullAt":…}`),
|
|
237
|
+
* or `null` before the first one.
|
|
238
|
+
*/
|
|
239
|
+
cursor(calendarId: number): Promise<string | null>;
|
|
240
|
+
/** `null` forgets the cursor, so the next sync is a full list. */
|
|
241
|
+
setCursor(calendarId: number, value: string | null, now: number): Promise<void>;
|
|
215
242
|
events(filter?: EventFilter): Promise<Event[]>;
|
|
216
243
|
event(id: number): Promise<EventDetails | null>;
|
|
217
244
|
/** The owner's link of an entry to an event; no rule replaces it. */
|
package/dist/meetings/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { calendarText, ENTRY_METADATA_KEYS, entryMetadata, withoutLinkQueries }
|
|
|
4
4
|
export type { MeetingContextStore, MeetingEventLinkInput, MeetingEventLinkReceipt, ParticipantPerson, ParticipantPersonInput, ParticipantPersonLinkInput, ParticipantPersonLinkReceipt, } from "./context-store.js";
|
|
5
5
|
export { type MeetingCueIdentity, meetingCueFingerprint } from "./cue-fingerprint.js";
|
|
6
6
|
export { EVENT_QUERY } from "./event-query.js";
|
|
7
|
-
export type { CalendarInput, EntryAttachment, EntryAttachmentInput, EntryInput, EntryKind, EntryParticipantInput, EntryRevision, EntryStatus, EventDetails, EventFilter, EventStore, LinkReview, ReviewDecision, StoredCalendar, StoredEntry, StoredParticipant, } from "./event-store.js";
|
|
7
|
+
export type { CalendarAccessRole, CalendarInput, EntryAttachment, EntryAttachmentInput, EntryEventType, EntryInput, EntryKind, EntryParticipantInput, EntryRevision, EntryStatus, EntryTransparency, EntryVisibility, EventDetails, EventFilter, EventStore, LinkReview, ParticipantResponse, ReviewDecision, StoredCalendar, StoredEntry, StoredParticipant, } from "./event-store.js";
|
|
8
8
|
export { linkEvent } from "./events.js";
|
|
9
9
|
export { exportMeeting, type MeetingExport, type MeetingExportFormat, type MeetingExportOptions, serializeVtt, } from "./export.js";
|
|
10
10
|
export { type FindingEvidence, type FindingOutcome, findingHash, MEETING_FINDINGS_KIND, type MeetingFactFinding, type MeetingFinding, type MeetingFindingsInput, type MeetingFindingsReceipt, type MeetingTaskFinding, meetingFindingFingerprint, validateMeetingFindings, } from "./findings.js";
|
|
@@ -16,20 +16,73 @@ export const eventStoreContract = (make) => {
|
|
|
16
16
|
name,
|
|
17
17
|
async run() {
|
|
18
18
|
const fixture = await make();
|
|
19
|
-
const [calendar] = await fixture.store.saveCalendars(fixture.accountId, [
|
|
19
|
+
const [calendar] = await fixture.store.saveCalendars(fixture.accountId, [
|
|
20
|
+
{
|
|
21
|
+
externalId: "primary",
|
|
22
|
+
title: "Work",
|
|
23
|
+
timezone: "UTC",
|
|
24
|
+
accessRole: "owner",
|
|
25
|
+
primary: true,
|
|
26
|
+
syncByDefault: true,
|
|
27
|
+
},
|
|
28
|
+
], 1);
|
|
20
29
|
await check({ ...fixture, calendar: calendar.id });
|
|
21
30
|
},
|
|
22
31
|
});
|
|
23
|
-
const second = async ({ store, accountId }, externalId = "personal") => (await store.saveCalendars(accountId, [
|
|
32
|
+
const second = async ({ store, accountId }, externalId = "personal") => (await store.saveCalendars(accountId, [
|
|
33
|
+
{
|
|
34
|
+
externalId,
|
|
35
|
+
title: externalId,
|
|
36
|
+
timezone: "UTC",
|
|
37
|
+
accessRole: "owner",
|
|
38
|
+
primary: false,
|
|
39
|
+
syncByDefault: true,
|
|
40
|
+
},
|
|
41
|
+
], 1))[0].id;
|
|
24
42
|
const later = CONTRACT_NOW + 24 * HOUR;
|
|
25
43
|
const earlier = CONTRACT_NOW - 24 * HOUR;
|
|
26
44
|
return [
|
|
27
|
-
test("a calendar
|
|
28
|
-
await store.saveCalendars(accountId, [
|
|
45
|
+
test("a new calendar syncs as its adapter decides, and a later list keeps the owner's choice", async ({ store, accountId, }) => {
|
|
46
|
+
await store.saveCalendars(accountId, [
|
|
47
|
+
{
|
|
48
|
+
externalId: "team",
|
|
49
|
+
title: "Team",
|
|
50
|
+
timezone: null,
|
|
51
|
+
accessRole: "reader",
|
|
52
|
+
primary: false,
|
|
53
|
+
syncByDefault: false,
|
|
54
|
+
},
|
|
55
|
+
], 2);
|
|
29
56
|
assert.deepEqual((await store.calendars(accountId)).map(({ externalId, synced }) => [externalId, synced]), [
|
|
30
57
|
["primary", true],
|
|
31
58
|
["team", false],
|
|
32
59
|
]);
|
|
60
|
+
const team = (await store.calendars(accountId)).find(({ externalId }) => externalId === "team");
|
|
61
|
+
await store.setCalendarSynced(team?.id, true, 3);
|
|
62
|
+
await store.saveCalendars(accountId, [
|
|
63
|
+
{
|
|
64
|
+
externalId: "team",
|
|
65
|
+
title: "Team",
|
|
66
|
+
timezone: null,
|
|
67
|
+
accessRole: "free-busy",
|
|
68
|
+
primary: false,
|
|
69
|
+
syncByDefault: false,
|
|
70
|
+
},
|
|
71
|
+
], 4);
|
|
72
|
+
const again = (await store.calendars(accountId)).find(({ externalId }) => externalId === "team");
|
|
73
|
+
assert.deepEqual([again?.synced, again?.accessRole], [true, "free-busy"]);
|
|
74
|
+
}),
|
|
75
|
+
test("calendar words are kept in their neutral spelling", async ({ store, calendar }) => {
|
|
76
|
+
const [entry] = await store.saveEntries(calendar, [
|
|
77
|
+
single("ooo", later, {
|
|
78
|
+
eventType: "out-of-office",
|
|
79
|
+
ownResponse: "needs-action",
|
|
80
|
+
participants: [{ email: "alice@example.com", response: "delegated" }],
|
|
81
|
+
}),
|
|
82
|
+
], 2);
|
|
83
|
+
assert.deepEqual([entry?.eventType, entry?.ownResponse], ["out-of-office", "needs-action"]);
|
|
84
|
+
const details = await store.event(entry?.eventId);
|
|
85
|
+
assert.equal(details?.participants[0]?.response, "delegated");
|
|
33
86
|
}),
|
|
34
87
|
test("rule 1: the same calendar and id is the same entry; a change keeps the old values", async ({ store, calendar, }) => {
|
|
35
88
|
const [first] = await store.saveEntries(calendar, [single("e1", later)], 2);
|
|
@@ -124,6 +177,32 @@ export const eventStoreContract = (make) => {
|
|
|
124
177
|
const after = await store.event(future?.eventId);
|
|
125
178
|
assert.equal(after?.event.status, "cancelled");
|
|
126
179
|
}),
|
|
180
|
+
test("entries removed by id: past stays as history, future cancels, a series cancels its slots", async ({ store, calendar, }) => {
|
|
181
|
+
const series = { ...single("s", earlier - HOUR), kind: "series", recurrence: ["RRULE:FREQ=DAILY"] };
|
|
182
|
+
const slot = (at) => single(`s_${at}`, at, { kind: "occurrence", seriesExternalId: "s", originalStartsAt: at });
|
|
183
|
+
const [past, future, , , slotLater] = await store.saveEntries(calendar, [single("p", earlier), single("f", later), series, slot(earlier), slot(later)], 2);
|
|
184
|
+
assert.equal(await store.removeEntries(calendar, ["p", "f", "s", "unknown"], CONTRACT_NOW), 3);
|
|
185
|
+
const happened = (await store.event(past?.eventId))?.event;
|
|
186
|
+
assert.equal(happened?.sourceDeletedAt, CONTRACT_NOW);
|
|
187
|
+
assert.equal(happened?.title, "Example review");
|
|
188
|
+
assert.equal((await store.event(future?.eventId))?.event.status, "cancelled");
|
|
189
|
+
const occurrence = await store.event(slotLater?.eventId);
|
|
190
|
+
assert.deepEqual([occurrence?.event.status, occurrence?.entries[0]?.status], ["cancelled", "cancelled"]);
|
|
191
|
+
assert.equal(occurrence?.entries[0]?.deletedAt, null);
|
|
192
|
+
assert.equal(await store.removeEntries(calendar, ["p"], CONTRACT_NOW + 1), 0);
|
|
193
|
+
}),
|
|
194
|
+
test("a calendar's cursor is its own, and null forgets it", async (fixture) => {
|
|
195
|
+
const { store, calendar } = fixture;
|
|
196
|
+
const other = await second(fixture);
|
|
197
|
+
assert.equal(await store.cursor(calendar), null);
|
|
198
|
+
const value = JSON.stringify({ v: 1, token: "token-1", window: { from: earlier, to: later }, fullAt: 2 });
|
|
199
|
+
await store.setCursor(calendar, value, 2);
|
|
200
|
+
await store.setCursor(other, "other", 2);
|
|
201
|
+
assert.equal(await store.cursor(calendar), value);
|
|
202
|
+
await store.setCursor(calendar, null, 3);
|
|
203
|
+
assert.equal(await store.cursor(calendar), null);
|
|
204
|
+
assert.equal(await store.cursor(other), "other");
|
|
205
|
+
}),
|
|
127
206
|
test("a full list marks the entries it did not return, plain occurrences only inside the window", async ({ store, calendar, }) => {
|
|
128
207
|
const outside = later + 30 * 24 * HOUR;
|
|
129
208
|
const [kept, gone, far] = await store.saveEntries(calendar, [
|
package/dist/sends/guard.d.ts
CHANGED
|
@@ -93,6 +93,8 @@ export interface GuardSettings {
|
|
|
93
93
|
readOnlyLock?: string;
|
|
94
94
|
permissions: Readonly<Record<PermissionKey, Level>>;
|
|
95
95
|
permissionSources: Readonly<Record<PermissionKey, string>>;
|
|
96
|
+
/** A CLI's own defaults (`commandPermissions`); without them a key falls back to the messenger's. */
|
|
97
|
+
permissionDefaults?: Readonly<Record<PermissionKey, Level>>;
|
|
96
98
|
sendsPerHour: number;
|
|
97
99
|
}
|
|
98
100
|
/**
|
package/dist/sends/guard.js
CHANGED
|
@@ -259,6 +259,7 @@ export const guardFor = (app, settings, warn, env = process.env, ask) => sendGua
|
|
|
259
259
|
...(settings.readOnlyLock ? { readOnlyLock: settings.readOnlyLock } : {}),
|
|
260
260
|
permissions: () => settings.permissions,
|
|
261
261
|
permissionSources: settings.permissionSources,
|
|
262
|
+
...(settings.permissionDefaults ? { permissionDefaults: settings.permissionDefaults } : {}),
|
|
262
263
|
...(ask ? { ask } : {}),
|
|
263
264
|
sendsPerHour: settings.sendsPerHour,
|
|
264
265
|
journal: new SendJournal(sendsPathFor(app, settings.profile, env), { service: app.command, version: app.version }),
|
package/dist/services/index.d.ts
CHANGED
|
@@ -83,7 +83,7 @@ export { searchStore } from "./messages-archive-search.js";
|
|
|
83
83
|
export type { ModerateOptions, ModerationService, ShownRules } from "./moderation.js";
|
|
84
84
|
export { moderationService } from "./moderation.js";
|
|
85
85
|
export type { ContactSync, PeopleService } from "./people.js";
|
|
86
|
-
export { peopleService, phoneOf } from "./people.js";
|
|
86
|
+
export { linkPeople, peopleService, phoneOf } from "./people.js";
|
|
87
87
|
export type { ContextEvent, ContextMessage, ContextOptions, NotReadReason, PersonContext, SharedChat, } from "./person-context.js";
|
|
88
88
|
export { CONTEXT_BYTES, CONTEXT_MESSAGES, identityIn, personContext } from "./person-context.js";
|
|
89
89
|
export type { PersonPreparation, WaitingDialog } from "./person-preparation.js";
|
package/dist/services/index.js
CHANGED
|
@@ -58,7 +58,7 @@ export { chatIdIn, storedChatId } from "./message-resolution.js";
|
|
|
58
58
|
export { DELETE_AT_ONCE, messagesService } from "./messages.js";
|
|
59
59
|
export { searchStore } from "./messages-archive-search.js";
|
|
60
60
|
export { moderationService } from "./moderation.js";
|
|
61
|
-
export { peopleService, phoneOf } from "./people.js";
|
|
61
|
+
export { linkPeople, peopleService, phoneOf } from "./people.js";
|
|
62
62
|
export { CONTEXT_BYTES, CONTEXT_MESSAGES, identityIn, personContext } from "./person-context.js";
|
|
63
63
|
export { PREPARE_BYTES, personPreparation } from "./person-preparation.js";
|
|
64
64
|
export { personTimeline, SCOPES, TIMELINE_ITEMS, TIMELINE_MAX } from "./person-timeline.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Predicate } from "@wirecat/cli-base/query";
|
|
2
2
|
import type { Contact, Id, Member, Page, PersonCard, PersonProfile, PhoneBookEntry } from "../domain/models.js";
|
|
3
3
|
import { type Operated } from "../sends/guarded.js";
|
|
4
|
-
import type { PersonRecord } from "../store/store.js";
|
|
4
|
+
import type { AccountKey, MessageStore, PersonRecord } from "../store/store.js";
|
|
5
5
|
import type { PageWindow } from "./chats.js";
|
|
6
6
|
import { type ServiceDeps } from "./deps.js";
|
|
7
7
|
import { type ContextOptions, type Detail, type PersonContext, type PersonMessages } from "./person-context.js";
|
|
@@ -83,6 +83,11 @@ export interface PeopleService {
|
|
|
83
83
|
}
|
|
84
84
|
export declare const peopleService: (deps: ServiceDeps) => PeopleService;
|
|
85
85
|
/** Digits, with the `+` and the spaces, dashes and brackets people type dropped. */
|
|
86
|
+
/**
|
|
87
|
+
* The owner's link of two people, each read in the account's messenger unless prefixed (`max:Ana`, `email:ana@x.com`,
|
|
88
|
+
* `person:12`); `me` links the first to the owner. A tool without a messenger (a calendar CLI) calls it directly.
|
|
89
|
+
*/
|
|
90
|
+
export declare const linkPeople: (store: MessageStore, account: AccountKey, person: string, other: string, method?: string) => Promise<PersonRecord>;
|
|
86
91
|
export declare const phoneOf: (typed: string) => string;
|
|
87
92
|
/** The phone cut to its last four digits: enough to tell two accounts apart. */
|
|
88
93
|
export declare const maskedAccount: <T extends {
|
package/dist/services/people.js
CHANGED
|
@@ -109,17 +109,7 @@ export const peopleService = (deps) => {
|
|
|
109
109
|
...(detail ? { detail } : {}),
|
|
110
110
|
});
|
|
111
111
|
},
|
|
112
|
-
link: async (person, other, method = "manual") =>
|
|
113
|
-
const store = await deps.store();
|
|
114
|
-
const account = await deps.account();
|
|
115
|
-
const one = await identityIn(store, account, person);
|
|
116
|
-
if (other === "me")
|
|
117
|
-
return store.people.linkToOwner(one);
|
|
118
|
-
const two = await identityIn(store, account, other);
|
|
119
|
-
if (one.provider === two.provider && one.id === two.id)
|
|
120
|
-
throw new CliError("validation_error", "that is the same person twice");
|
|
121
|
-
return store.people.linkIdentities(one, two, { method, by: "owner" });
|
|
122
|
-
},
|
|
112
|
+
link: async (person, other, method = "manual") => linkPeople(await deps.store(), await deps.account(), person, other, method),
|
|
123
113
|
unlink: async (person) => {
|
|
124
114
|
const store = await deps.store();
|
|
125
115
|
return store.people.unlinkIdentity(await identityIn(store, await deps.account(), person), {
|
|
@@ -199,6 +189,19 @@ export const peopleService = (deps) => {
|
|
|
199
189
|
/** Numbers per request: one big list in one request is what a contact-scraping account sends. */
|
|
200
190
|
const IMPORT_CHUNK = 10;
|
|
201
191
|
/** Digits, with the `+` and the spaces, dashes and brackets people type dropped. */
|
|
192
|
+
/**
|
|
193
|
+
* The owner's link of two people, each read in the account's messenger unless prefixed (`max:Ana`, `email:ana@x.com`,
|
|
194
|
+
* `person:12`); `me` links the first to the owner. A tool without a messenger (a calendar CLI) calls it directly.
|
|
195
|
+
*/
|
|
196
|
+
export const linkPeople = async (store, account, person, other, method = "manual") => {
|
|
197
|
+
const one = await identityIn(store, account, person);
|
|
198
|
+
if (other === "me")
|
|
199
|
+
return store.people.linkToOwner(one);
|
|
200
|
+
const two = await identityIn(store, account, other);
|
|
201
|
+
if (one.provider === two.provider && one.id === two.id)
|
|
202
|
+
throw new CliError("validation_error", "that is the same person twice");
|
|
203
|
+
return store.people.linkIdentities(one, two, { method, by: "owner" });
|
|
204
|
+
};
|
|
202
205
|
export const phoneOf = (typed) => {
|
|
203
206
|
const digits = typed.replace(/[\s()+-]/g, "");
|
|
204
207
|
if (!/^\d{6,15}$/.test(digits)) {
|
|
@@ -22,6 +22,7 @@ const entryOf = (row) => ({
|
|
|
22
22
|
calendarId: Number(row.calendar_id),
|
|
23
23
|
kind: row.kind,
|
|
24
24
|
seriesEntryId: int(row.series_entry_id),
|
|
25
|
+
seriesExternalId: str(row.series_external_id),
|
|
25
26
|
eventId: int(row.event_id),
|
|
26
27
|
eventSeriesId: int(row.event_series_id),
|
|
27
28
|
externalId: String(row.external_id),
|
|
@@ -94,6 +95,7 @@ const attachmentOf = (row) => ({
|
|
|
94
95
|
});
|
|
95
96
|
const TRACKED = ["title", "starts_at", "ends_at", "location", "status", "recurrence", "recurrence_ends_at"];
|
|
96
97
|
const flag = (value) => (value ? 1 : 0);
|
|
98
|
+
const cursorKey = (calendarId) => `calendar:${calendarId}`;
|
|
97
99
|
const addressOf = (email) => email.trim().toLowerCase();
|
|
98
100
|
/** The `email` contact of an address: the same one mail uses, made when missing; it gets a name only when new. */
|
|
99
101
|
const contactOf = (context, email, name) => {
|
|
@@ -331,7 +333,7 @@ export const eventStoreOver = (context) => {
|
|
|
331
333
|
.get(seriesEntryId, input.originalStartsAt);
|
|
332
334
|
return database
|
|
333
335
|
.prepare("SELECT * FROM calendar_entries WHERE calendar_id = ? AND series_entry_id IS NULL AND original_starts_at = ? " +
|
|
334
|
-
"AND
|
|
336
|
+
"AND series_external_id = ?")
|
|
335
337
|
.get(calendarId, input.originalStartsAt, input.seriesExternalId ?? null);
|
|
336
338
|
};
|
|
337
339
|
const saveEntry = (calendarId, input, now) => {
|
|
@@ -347,6 +349,7 @@ export const eventStoreOver = (context) => {
|
|
|
347
349
|
const values = {
|
|
348
350
|
kind: input.kind,
|
|
349
351
|
series_entry_id: seriesEntryId,
|
|
352
|
+
series_external_id: input.kind === "occurrence" ? (input.seriesExternalId ?? null) : null,
|
|
350
353
|
external_id: input.externalId,
|
|
351
354
|
ical_uid: input.icalUid ?? null,
|
|
352
355
|
original_starts_at: input.originalStartsAt ?? null,
|
|
@@ -371,17 +374,11 @@ export const eventStoreOver = (context) => {
|
|
|
371
374
|
join_url: joinUrl,
|
|
372
375
|
join_key: joinUrl ? (joinKeyOf(joinUrl) ?? "") : null,
|
|
373
376
|
conference: input.conference ?? null,
|
|
374
|
-
// The calendar's own page of the event names it in the query (`eid`), which opens nothing on its own.
|
|
375
377
|
url: input.url ?? null,
|
|
376
378
|
sequence: input.sequence ?? null,
|
|
377
379
|
etag: input.etag ?? null,
|
|
378
380
|
external_updated_at: input.externalUpdatedAt ?? null,
|
|
379
|
-
metadata: toJson(
|
|
380
|
-
...entryMetadata(input.metadata),
|
|
381
|
-
...(input.kind === "occurrence" && input.seriesExternalId
|
|
382
|
-
? { recurring_event_id: input.seriesExternalId }
|
|
383
|
-
: {}),
|
|
384
|
-
}),
|
|
381
|
+
metadata: toJson(entryMetadata(input.metadata)),
|
|
385
382
|
seen_at: now,
|
|
386
383
|
deleted_at: null,
|
|
387
384
|
};
|
|
@@ -415,23 +412,39 @@ export const eventStoreOver = (context) => {
|
|
|
415
412
|
if (input.kind === "series")
|
|
416
413
|
database
|
|
417
414
|
.prepare("UPDATE calendar_entries SET series_entry_id = ?, updated_at = ? WHERE calendar_id = ? AND kind = 'occurrence' " +
|
|
418
|
-
"AND series_entry_id IS NULL AND
|
|
415
|
+
"AND series_entry_id IS NULL AND series_external_id = ?")
|
|
419
416
|
.run(id, now, calendarId, input.externalId);
|
|
420
417
|
return entryOf(entryRow(id));
|
|
421
418
|
};
|
|
419
|
+
const cancel = (row, now) => {
|
|
420
|
+
database
|
|
421
|
+
.prepare(`INSERT INTO calendar_entry_revisions (entry_id, title, starts_at, ends_at, location, status, recurrence,
|
|
422
|
+
recurrence_ends_at, external_updated_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
423
|
+
.run(Number(row.id), str(row.title), int(row.starts_at), int(row.ends_at), str(row.location), str(row.status), str(row.recurrence), int(row.recurrence_ends_at), int(row.external_updated_at), now);
|
|
424
|
+
database
|
|
425
|
+
.prepare("UPDATE calendar_entries SET status = 'cancelled', updated_at = ? WHERE id = ?")
|
|
426
|
+
.run(now, Number(row.id));
|
|
427
|
+
};
|
|
422
428
|
/** A series cancelled at the provider cancels its occurrences: past ones stay as history, future ones cancel. */
|
|
423
|
-
const cancelOccurrences = (
|
|
429
|
+
const cancelOccurrences = (seriesEntryId, now) => {
|
|
424
430
|
const occurrences = database
|
|
425
431
|
.prepare("SELECT * FROM calendar_entries WHERE series_entry_id = ? AND status <> 'cancelled' AND deleted_at IS NULL")
|
|
426
|
-
.all(
|
|
427
|
-
for (const row of occurrences)
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
432
|
+
.all(seriesEntryId);
|
|
433
|
+
for (const row of occurrences)
|
|
434
|
+
cancel(row, now);
|
|
435
|
+
};
|
|
436
|
+
/** The events and series of these entries and of their occurrences take their fields again. */
|
|
437
|
+
const refreshAround = (entryIds, now) => {
|
|
438
|
+
const ids = JSON.stringify(entryIds);
|
|
439
|
+
const touched = database
|
|
440
|
+
.prepare(`SELECT DISTINCT ce.event_id, ce.event_series_id FROM calendar_entries ce
|
|
441
|
+
WHERE ce.id IN (SELECT value FROM json_each(?)) OR ce.series_entry_id IN (SELECT value FROM json_each(?))`)
|
|
442
|
+
.all(ids, ids);
|
|
443
|
+
for (const row of touched) {
|
|
444
|
+
if (row.event_series_id != null)
|
|
445
|
+
refreshSeries(Number(row.event_series_id), now);
|
|
446
|
+
if (row.event_id != null)
|
|
447
|
+
refreshEvent(Number(row.event_id), now);
|
|
435
448
|
}
|
|
436
449
|
};
|
|
437
450
|
return {
|
|
@@ -442,7 +455,7 @@ export const eventStoreOver = (context) => {
|
|
|
442
455
|
access_role = excluded.access_role, primary_calendar = excluded.primary_calendar, seen_at = excluded.seen_at,
|
|
443
456
|
deleted_at = NULL, updated_at = excluded.updated_at
|
|
444
457
|
RETURNING *`);
|
|
445
|
-
return calendars.map((one) => calendarOf(upsert.get(accountId, one.externalId, one.title, one.timezone, one.accessRole, flag(one.primary), flag(one.
|
|
458
|
+
return calendars.map((one) => calendarOf(upsert.get(accountId, one.externalId, one.title, one.timezone, one.accessRole, flag(one.primary), flag(one.syncByDefault), now, now, now)));
|
|
446
459
|
}),
|
|
447
460
|
calendars: async (accountId) => database
|
|
448
461
|
.prepare("SELECT * FROM calendars WHERE account_id = ? ORDER BY primary_calendar DESC, title, id")
|
|
@@ -466,22 +479,28 @@ export const eventStoreOver = (context) => {
|
|
|
466
479
|
});
|
|
467
480
|
for (const entry of saved)
|
|
468
481
|
if (entry.kind === "series" && entry.status === "cancelled")
|
|
469
|
-
cancelOccurrences(entry, now);
|
|
482
|
+
cancelOccurrences(entry.id, now);
|
|
470
483
|
const current = saved.map(({ id }) => entryOf(entryRow(id)));
|
|
471
484
|
for (const entry of current)
|
|
472
485
|
link(entry, now);
|
|
473
|
-
|
|
474
|
-
.prepare(`SELECT DISTINCT ce.event_id, ce.event_series_id FROM calendar_entries ce
|
|
475
|
-
WHERE ce.id IN (SELECT value FROM json_each(?)) OR ce.series_entry_id IN (SELECT value FROM json_each(?))`)
|
|
476
|
-
.all(JSON.stringify(current.map(({ id }) => id)), JSON.stringify(current.map(({ id }) => id)));
|
|
477
|
-
for (const row of touched) {
|
|
478
|
-
if (row.event_series_id != null)
|
|
479
|
-
refreshSeries(Number(row.event_series_id), now);
|
|
480
|
-
if (row.event_id != null)
|
|
481
|
-
refreshEvent(Number(row.event_id), now);
|
|
482
|
-
}
|
|
486
|
+
refreshAround(current.map(({ id }) => id), now);
|
|
483
487
|
return entries.map((input) => entryOf(entryRow(ids.get(input))));
|
|
484
488
|
}),
|
|
489
|
+
removeEntries: async (calendarId, externalIds, now) => write(database, () => {
|
|
490
|
+
calendarRow(calendarId);
|
|
491
|
+
const removed = database
|
|
492
|
+
.prepare(`SELECT * FROM calendar_entries WHERE calendar_id = ? AND external_id IN (SELECT value FROM json_each(?))
|
|
493
|
+
AND status <> 'cancelled' AND deleted_at IS NULL`)
|
|
494
|
+
.all(calendarId, JSON.stringify(externalIds));
|
|
495
|
+
for (const row of removed) {
|
|
496
|
+
cancel(row, now);
|
|
497
|
+
database.prepare("UPDATE calendar_entries SET seen_at = ? WHERE id = ?").run(now, Number(row.id));
|
|
498
|
+
if (row.kind === "series")
|
|
499
|
+
cancelOccurrences(Number(row.id), now);
|
|
500
|
+
}
|
|
501
|
+
refreshAround(removed.map((row) => Number(row.id)), now);
|
|
502
|
+
return removed.length;
|
|
503
|
+
}),
|
|
485
504
|
markUnseen: async (calendarId, seenBefore, window, now) => write(database, () => {
|
|
486
505
|
const marked = database
|
|
487
506
|
.prepare(`UPDATE calendar_entries SET deleted_at = ?, updated_at = ? WHERE calendar_id = ? AND deleted_at IS NULL
|
|
@@ -494,6 +513,24 @@ export const eventStoreOver = (context) => {
|
|
|
494
513
|
refreshEvent(eventId, now);
|
|
495
514
|
return marked.length;
|
|
496
515
|
}),
|
|
516
|
+
cursor: async (calendarId) => read(database, () => {
|
|
517
|
+
const { account_id } = calendarRow(calendarId);
|
|
518
|
+
return str(database
|
|
519
|
+
.prepare("SELECT value FROM sync_cursors WHERE account_id = ? AND key = ?")
|
|
520
|
+
.get(Number(account_id), cursorKey(calendarId))?.value);
|
|
521
|
+
}),
|
|
522
|
+
setCursor: async (calendarId, value, now) => write(database, () => {
|
|
523
|
+
const accountId = Number(calendarRow(calendarId).account_id);
|
|
524
|
+
if (value === null)
|
|
525
|
+
database
|
|
526
|
+
.prepare("DELETE FROM sync_cursors WHERE account_id = ? AND key = ?")
|
|
527
|
+
.run(accountId, cursorKey(calendarId));
|
|
528
|
+
else
|
|
529
|
+
database
|
|
530
|
+
.prepare(`INSERT INTO sync_cursors (account_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?)
|
|
531
|
+
ON CONFLICT (account_id, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`)
|
|
532
|
+
.run(accountId, cursorKey(calendarId), value, now, now);
|
|
533
|
+
}),
|
|
497
534
|
events: async (filter = {}) => {
|
|
498
535
|
const clauses = ["e.deleted_at IS NULL"];
|
|
499
536
|
const params = [];
|
|
@@ -45,6 +45,8 @@ export const MANIFEST = [
|
|
|
45
45
|
{ name: "20261025072644_version-33-calendars-triggers", version: 33, minCompatible: 32 },
|
|
46
46
|
{ name: "20261025182500_version-34-vector-index", version: 34, minCompatible: 34 },
|
|
47
47
|
{ name: "20261025182501_version-34-vector-index-triggers", version: 34, minCompatible: 34 },
|
|
48
|
+
{ name: "20261027102259_version-35-calendar-neutral", version: 35, minCompatible: 34 },
|
|
49
|
+
{ name: "20261027102302_version-35-calendar-neutral-data", version: 35, minCompatible: 34 },
|
|
48
50
|
];
|
|
49
51
|
export const generatedMigrations = (manifest = MANIFEST, generated = GENERATED) => {
|
|
50
52
|
const migrations = [];
|
|
@@ -761,5 +761,22 @@ export const GENERATED = [
|
|
|
761
761
|
"CREATE TRIGGER vector_index_conversation_au AFTER UPDATE OF chat_id ON conversations\n WHEN old.chat_id IS NOT new.chat_id BEGIN\n INSERT INTO vector_index_redo (chunk_id)\n SELECT id FROM chunks WHERE chunkable_type = 'conversation' AND chunkable_id = new.id;\n DELETE FROM vector_index_redo;\nEND;",
|
|
762
762
|
"-- Images and conversations are indexed under their chat's account; messages carry their own.\nCREATE TRIGGER vector_index_chat_au AFTER UPDATE OF account_id ON chats WHEN old.account_id IS NOT new.account_id BEGIN\n INSERT INTO vector_index_redo (chunk_id)\n SELECT k.id FROM messages m JOIN attachments a ON a.attachable_type = 'message' AND a.attachable_id = m.id\n JOIN chunks k ON k.chunkable_type = 'image' AND k.chunkable_id = a.id WHERE m.chat_id = new.id\n UNION ALL SELECT k.id FROM conversations c JOIN chunks k ON k.chunkable_type = 'conversation' AND k.chunkable_id = c.id\n WHERE c.chat_id = new.id;\n DELETE FROM vector_index_redo;\nEND;"
|
|
763
763
|
]
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
"name": "20261027102259_version-35-calendar-neutral",
|
|
767
|
+
"statements": [
|
|
768
|
+
"ALTER TABLE `calendar_entries` ADD `series_external_id` text;",
|
|
769
|
+
"CREATE INDEX `calendar_entries_by_calendar_id_series_external_id` ON `calendar_entries` (`calendar_id`,`series_external_id`) WHERE series_external_id IS NOT NULL;"
|
|
770
|
+
]
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
"name": "20261027102302_version-35-calendar-neutral-data",
|
|
774
|
+
"statements": [
|
|
775
|
+
"-- `metadata.recurring_event_id` stays on the rows it is on: a version-34 build may still open this file and reads it.\nUPDATE calendar_entries SET series_external_id = json_extract(metadata, '$.recurring_event_id')\n WHERE kind = 'occurrence' AND json_valid(metadata) AND json_extract(metadata, '$.recurring_event_id') IS NOT NULL;",
|
|
776
|
+
"UPDATE calendar_entries SET event_type = CASE event_type\n WHEN 'outOfOffice' THEN 'out-of-office'\n WHEN 'focusTime' THEN 'focus'\n WHEN 'workingLocation' THEN 'working-location'\n WHEN 'fromGmail' THEN 'from-mail'\n END\n WHERE event_type IN ('outOfOffice', 'focusTime', 'workingLocation', 'fromGmail');",
|
|
777
|
+
"UPDATE calendar_entries SET own_response = 'needs-action' WHERE own_response = 'needsAction';",
|
|
778
|
+
"UPDATE calendar_entry_participants SET response = 'needs-action' WHERE response = 'needsAction';",
|
|
779
|
+
"UPDATE calendars SET access_role = 'free-busy' WHERE access_role = 'freeBusyReader';"
|
|
780
|
+
]
|
|
764
781
|
}
|
|
765
782
|
];
|
|
@@ -34,7 +34,7 @@ export interface Corpus {
|
|
|
34
34
|
/** What an event `e` is searched by besides its title: the cleaned description, the place and the attendees' names. */
|
|
35
35
|
export declare const EVENT_TEXT: string;
|
|
36
36
|
/** Calendar entry types that are listed, never searched (requirements 3.4). */
|
|
37
|
-
export declare const UNSEARCHED_EVENT_TYPES: readonly ["
|
|
37
|
+
export declare const UNSEARCHED_EVENT_TYPES: readonly ["working-location", "focus", "out-of-office", "birthday"];
|
|
38
38
|
/** Visibilities whose entry, when it carries no text, is a busy-only slot: stored, never searched (requirements 3.4). */
|
|
39
39
|
export declare const BUSY_ONLY_VISIBILITIES: readonly ["private", "confidential"];
|
|
40
40
|
/**
|
|
@@ -14,7 +14,12 @@ export const EVENT_TEXT = "trim(coalesce(e.description, '') || ' ' || coalesce(e
|
|
|
14
14
|
"SELECT group_concat(p.name, ' ') FROM calendar_entries ce JOIN calendar_entry_participants p ON p.entry_id = ce.id " +
|
|
15
15
|
"WHERE ce.event_id = e.id AND ce.deleted_at IS NULL AND p.resource = 0 AND p.name IS NOT NULL), ''))";
|
|
16
16
|
/** Calendar entry types that are listed, never searched (requirements 3.4). */
|
|
17
|
-
export const UNSEARCHED_EVENT_TYPES = [
|
|
17
|
+
export const UNSEARCHED_EVENT_TYPES = [
|
|
18
|
+
"working-location",
|
|
19
|
+
"focus",
|
|
20
|
+
"out-of-office",
|
|
21
|
+
"birthday",
|
|
22
|
+
];
|
|
18
23
|
/** Visibilities whose entry, when it carries no text, is a busy-only slot: stored, never searched (requirements 3.4). */
|
|
19
24
|
export const BUSY_ONLY_VISIBILITIES = ["private", "confidential"];
|
|
20
25
|
const sqlList = (values) => values.map((value) => `'${value}'`).join(", ");
|
|
@@ -8930,6 +8930,22 @@ export declare const calendarEntries: import("drizzle-orm/sqlite-core").SQLiteTa
|
|
|
8930
8930
|
identity: undefined;
|
|
8931
8931
|
generated: undefined;
|
|
8932
8932
|
}, {}>;
|
|
8933
|
+
seriesExternalId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
8934
|
+
name: string;
|
|
8935
|
+
tableName: "calendar_entries";
|
|
8936
|
+
dataType: "string";
|
|
8937
|
+
data: string;
|
|
8938
|
+
driverParam: string;
|
|
8939
|
+
notNull: false;
|
|
8940
|
+
hasDefault: false;
|
|
8941
|
+
isPrimaryKey: false;
|
|
8942
|
+
isAutoincrement: false;
|
|
8943
|
+
hasRuntimeDefault: false;
|
|
8944
|
+
enumValues: [string, ...string[]];
|
|
8945
|
+
baseColumn: never;
|
|
8946
|
+
identity: undefined;
|
|
8947
|
+
generated: undefined;
|
|
8948
|
+
}, {}>;
|
|
8933
8949
|
};
|
|
8934
8950
|
dialect: 'sqlite';
|
|
8935
8951
|
isAlias: false;
|
|
@@ -911,12 +911,16 @@ export const calendarEntries = sqliteTable("calendar_entries", {
|
|
|
911
911
|
createdAt: integer("created_at").notNull(),
|
|
912
912
|
updatedAt: integer("updated_at").notNull(),
|
|
913
913
|
deletedAt: integer("deleted_at"),
|
|
914
|
+
seriesExternalId: text("series_external_id"),
|
|
914
915
|
}, (table) => [
|
|
915
916
|
unique().on(table.calendarId, table.externalId),
|
|
916
917
|
unique().on(table.seriesEntryId, table.originalStartsAt),
|
|
917
918
|
index("calendar_entries_by_calendar_id_starts_at").on(table.calendarId, table.startsAt),
|
|
918
919
|
index("calendar_entries_by_ical_uid").on(table.icalUid).where(sql `ical_uid IS NOT NULL`),
|
|
919
920
|
index("calendar_entries_by_join_key").on(table.joinKey).where(sql `join_key IS NOT NULL`),
|
|
921
|
+
index("calendar_entries_by_calendar_id_series_external_id")
|
|
922
|
+
.on(table.calendarId, table.seriesExternalId)
|
|
923
|
+
.where(sql `series_external_id IS NOT NULL`),
|
|
920
924
|
index("calendar_entries_by_event_id").on(table.eventId),
|
|
921
925
|
index("calendar_entries_by_event_series_id").on(table.eventSeriesId),
|
|
922
926
|
index("calendar_entries_by_event_type_event_id").on(table.eventType, table.eventId),
|