@juppytt/fws 0.1.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.
@@ -0,0 +1,313 @@
1
+ import type { FwsStore, GmailLabel, GmailMessage, CalendarEvent, DriveFile, TaskList, Task, Spreadsheet, Person, ContactGroup } from './types.js';
2
+ import { generateEtag } from '../util/id.js';
3
+
4
+ const SYSTEM_LABELS: GmailLabel[] = [
5
+ { id: 'INBOX', name: 'INBOX', type: 'system' },
6
+ { id: 'SENT', name: 'SENT', type: 'system' },
7
+ { id: 'DRAFT', name: 'DRAFT', type: 'system' },
8
+ { id: 'TRASH', name: 'TRASH', type: 'system' },
9
+ { id: 'SPAM', name: 'SPAM', type: 'system' },
10
+ { id: 'STARRED', name: 'STARRED', type: 'system' },
11
+ { id: 'UNREAD', name: 'UNREAD', type: 'system' },
12
+ { id: 'IMPORTANT', name: 'IMPORTANT', type: 'system' },
13
+ { id: 'CATEGORY_PERSONAL', name: 'CATEGORY_PERSONAL', type: 'system' },
14
+ { id: 'CATEGORY_SOCIAL', name: 'CATEGORY_SOCIAL', type: 'system' },
15
+ { id: 'CATEGORY_PROMOTIONS', name: 'CATEGORY_PROMOTIONS', type: 'system' },
16
+ { id: 'CATEGORY_UPDATES', name: 'CATEGORY_UPDATES', type: 'system' },
17
+ { id: 'CATEGORY_FORUMS', name: 'CATEGORY_FORUMS', type: 'system' },
18
+ ];
19
+
20
+ const DEFAULT_EMAIL = 'testuser@example.com';
21
+
22
+ function makeMessage(id: string, threadId: string, historyId: number, opts: {
23
+ from: string; to: string; subject: string; body: string;
24
+ labels: string[]; date: string;
25
+ }): GmailMessage {
26
+ return {
27
+ id,
28
+ threadId,
29
+ labelIds: opts.labels,
30
+ snippet: opts.body.slice(0, 100),
31
+ historyId: String(historyId),
32
+ internalDate: String(new Date(opts.date).getTime()),
33
+ sizeEstimate: opts.body.length,
34
+ payload: {
35
+ partId: '',
36
+ mimeType: 'text/plain',
37
+ filename: '',
38
+ headers: [
39
+ { name: 'From', value: opts.from },
40
+ { name: 'To', value: opts.to },
41
+ { name: 'Subject', value: opts.subject },
42
+ { name: 'Date', value: opts.date },
43
+ ],
44
+ body: {
45
+ size: opts.body.length,
46
+ data: Buffer.from(opts.body).toString('base64url'),
47
+ },
48
+ },
49
+ };
50
+ }
51
+
52
+ function makeEvent(id: string, opts: {
53
+ summary: string; start: string; end: string;
54
+ description?: string; location?: string;
55
+ }): CalendarEvent {
56
+ return {
57
+ kind: 'calendar#event',
58
+ id,
59
+ status: 'confirmed',
60
+ summary: opts.summary,
61
+ description: opts.description,
62
+ location: opts.location,
63
+ start: { dateTime: opts.start },
64
+ end: { dateTime: opts.end },
65
+ created: '2026-04-01T00:00:00Z',
66
+ updated: '2026-04-01T00:00:00Z',
67
+ creator: { email: DEFAULT_EMAIL },
68
+ organizer: { email: DEFAULT_EMAIL, self: true },
69
+ etag: generateEtag(),
70
+ htmlLink: `https://calendar.google.com/event?eid=${id}`,
71
+ iCalUID: `${id}@example.com`,
72
+ };
73
+ }
74
+
75
+ function makeFile(id: string, opts: {
76
+ name: string; mimeType: string; parents?: string[];
77
+ }): DriveFile {
78
+ return {
79
+ kind: 'drive#file',
80
+ id,
81
+ name: opts.name,
82
+ mimeType: opts.mimeType,
83
+ parents: opts.parents || ['root'],
84
+ createdTime: '2026-04-01T00:00:00Z',
85
+ modifiedTime: '2026-04-01T00:00:00Z',
86
+ trashed: false,
87
+ starred: false,
88
+ owners: [{ emailAddress: DEFAULT_EMAIL, displayName: 'Test User' }],
89
+ };
90
+ }
91
+
92
+ export function createSeedStore(): FwsStore {
93
+ const labels: Record<string, GmailLabel> = {};
94
+ for (const label of SYSTEM_LABELS) {
95
+ labels[label.id] = { ...label };
96
+ }
97
+ labels['Label_projects'] = { id: 'Label_projects', name: 'Projects', type: 'user' };
98
+
99
+ const calendarId = DEFAULT_EMAIL;
100
+ const etag = generateEtag();
101
+
102
+ // --- Sample emails ---
103
+ const messages: Record<string, GmailMessage> = {};
104
+ const sampleMessages = [
105
+ makeMessage('msg001', 'thread001', 1001, {
106
+ from: 'alice@company.com', to: DEFAULT_EMAIL,
107
+ subject: 'Q3 Planning Meeting',
108
+ body: 'Hi, let\'s meet tomorrow at 2pm to discuss Q3 planning. I\'ve shared the agenda doc in Drive.',
109
+ labels: ['INBOX', 'UNREAD', 'IMPORTANT'], date: '2026-04-07T09:00:00Z',
110
+ }),
111
+ makeMessage('msg002', 'thread002', 1002, {
112
+ from: 'bob@company.com', to: DEFAULT_EMAIL,
113
+ subject: 'Code Review: auth refactor PR #42',
114
+ body: 'Please review the auth middleware refactor when you get a chance. The PR is ready.',
115
+ labels: ['INBOX', 'UNREAD'], date: '2026-04-07T10:30:00Z',
116
+ }),
117
+ makeMessage('msg003', 'thread003', 1003, {
118
+ from: 'notifications@github.com', to: DEFAULT_EMAIL,
119
+ subject: '[project/repo] CI pipeline failed on main',
120
+ body: 'Build #1234 failed. See details: https://github.com/project/repo/actions/runs/1234',
121
+ labels: ['INBOX', 'UNREAD', 'CATEGORY_UPDATES'], date: '2026-04-07T11:00:00Z',
122
+ }),
123
+ makeMessage('msg004', 'thread004', 1004, {
124
+ from: DEFAULT_EMAIL, to: 'alice@company.com',
125
+ subject: 'Re: Project proposal',
126
+ body: 'Looks good to me. Let\'s proceed with option B as discussed.',
127
+ labels: ['SENT'], date: '2026-04-06T16:00:00Z',
128
+ }),
129
+ makeMessage('msg005', 'thread005', 1005, {
130
+ from: 'hr@company.com', to: DEFAULT_EMAIL,
131
+ subject: 'Reminder: Submit timesheet by Friday',
132
+ body: 'Please submit your timesheet for this week by end of day Friday.',
133
+ labels: ['INBOX'], date: '2026-04-05T08:00:00Z',
134
+ }),
135
+ ];
136
+ for (const msg of sampleMessages) {
137
+ messages[msg.id] = msg;
138
+ }
139
+
140
+ // --- Sample events ---
141
+ const events: Record<string, CalendarEvent> = {};
142
+ const sampleEvents = [
143
+ makeEvent('evt001', {
144
+ summary: 'Daily Standup',
145
+ start: '2026-04-08T09:00:00Z', end: '2026-04-08T09:15:00Z',
146
+ }),
147
+ makeEvent('evt002', {
148
+ summary: 'Q3 Planning Meeting',
149
+ start: '2026-04-08T14:00:00Z', end: '2026-04-08T15:00:00Z',
150
+ location: 'Conference Room A',
151
+ description: 'Discuss Q3 roadmap and resource allocation',
152
+ }),
153
+ makeEvent('evt003', {
154
+ summary: '1:1 with Manager',
155
+ start: '2026-04-09T10:00:00Z', end: '2026-04-09T10:30:00Z',
156
+ }),
157
+ makeEvent('evt004', {
158
+ summary: 'Team Lunch',
159
+ start: '2026-04-10T12:00:00Z', end: '2026-04-10T13:00:00Z',
160
+ location: 'Cafeteria',
161
+ }),
162
+ ];
163
+ for (const evt of sampleEvents) {
164
+ events[evt.id] = evt;
165
+ }
166
+
167
+ // --- Sample drive files ---
168
+ const files: Record<string, DriveFile> = {};
169
+ const sampleFiles = [
170
+ makeFile('file001', { name: 'Q3 Planning Agenda', mimeType: 'application/vnd.google-apps.document' }),
171
+ makeFile('file002', { name: 'Budget 2026.xlsx', mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }),
172
+ makeFile('file003', { name: 'Architecture Diagram.png', mimeType: 'image/png' }),
173
+ makeFile('file004', { name: 'Meeting Notes', mimeType: 'application/vnd.google-apps.document', parents: ['folder001'] }),
174
+ makeFile('folder001', { name: 'Project Docs', mimeType: 'application/vnd.google-apps.folder' }),
175
+ ];
176
+ for (const file of sampleFiles) {
177
+ files[file.id] = file;
178
+ }
179
+
180
+ return {
181
+ gmail: {
182
+ profile: {
183
+ emailAddress: DEFAULT_EMAIL,
184
+ messagesTotal: sampleMessages.length,
185
+ threadsTotal: sampleMessages.length,
186
+ historyId: '1005',
187
+ },
188
+ messages,
189
+ labels,
190
+ nextHistoryId: 1006,
191
+ },
192
+ calendar: {
193
+ calendars: {
194
+ [calendarId]: {
195
+ kind: 'calendar#calendar',
196
+ id: calendarId,
197
+ summary: DEFAULT_EMAIL,
198
+ timeZone: 'UTC',
199
+ etag,
200
+ },
201
+ },
202
+ events: {
203
+ [calendarId]: events,
204
+ },
205
+ calendarList: {
206
+ [calendarId]: {
207
+ kind: 'calendar#calendarListEntry',
208
+ id: calendarId,
209
+ summary: DEFAULT_EMAIL,
210
+ timeZone: 'UTC',
211
+ accessRole: 'owner',
212
+ defaultReminders: [],
213
+ selected: true,
214
+ primary: true,
215
+ etag,
216
+ },
217
+ },
218
+ },
219
+ drive: {
220
+ files,
221
+ },
222
+ tasks: {
223
+ taskLists: {
224
+ 'default': {
225
+ kind: 'tasks#taskList',
226
+ id: 'default',
227
+ title: 'My Tasks',
228
+ updated: '2026-04-01T00:00:00Z',
229
+ selfLink: '',
230
+ },
231
+ },
232
+ tasks: {
233
+ 'default': {
234
+ 'task001': {
235
+ kind: 'tasks#task',
236
+ id: 'task001',
237
+ title: 'Review Q3 proposal',
238
+ updated: '2026-04-07T09:00:00Z',
239
+ selfLink: '',
240
+ status: 'needsAction',
241
+ due: '2026-04-10T00:00:00Z',
242
+ notes: 'Check budget section',
243
+ position: '00000000000000000001',
244
+ },
245
+ 'task002': {
246
+ kind: 'tasks#task',
247
+ id: 'task002',
248
+ title: 'Update documentation',
249
+ updated: '2026-04-06T14:00:00Z',
250
+ selfLink: '',
251
+ status: 'completed',
252
+ completed: '2026-04-06T16:00:00Z',
253
+ position: '00000000000000000002',
254
+ },
255
+ },
256
+ },
257
+ },
258
+ sheets: {
259
+ spreadsheets: {
260
+ 'sheet001': {
261
+ spreadsheetId: 'sheet001',
262
+ properties: {
263
+ title: 'Budget 2026',
264
+ locale: 'en_US',
265
+ timeZone: 'America/New_York',
266
+ },
267
+ sheets: [
268
+ {
269
+ properties: {
270
+ sheetId: 0,
271
+ title: 'Sheet1',
272
+ index: 0,
273
+ sheetType: 'GRID',
274
+ gridProperties: { rowCount: 100, columnCount: 26 },
275
+ },
276
+ },
277
+ ],
278
+ spreadsheetUrl: 'https://docs.google.com/spreadsheets/d/sheet001/edit',
279
+ },
280
+ },
281
+ },
282
+ people: {
283
+ contacts: {
284
+ 'people/c001': {
285
+ resourceName: 'people/c001',
286
+ etag: generateEtag(),
287
+ names: [{ displayName: 'Alice Johnson', givenName: 'Alice', familyName: 'Johnson' }],
288
+ emailAddresses: [{ value: 'alice@company.com', type: 'work' }],
289
+ phoneNumbers: [{ value: '+1-555-0101', type: 'work' }],
290
+ organizations: [{ name: 'Company Inc', title: 'Engineer' }],
291
+ },
292
+ 'people/c002': {
293
+ resourceName: 'people/c002',
294
+ etag: generateEtag(),
295
+ names: [{ displayName: 'Bob Smith', givenName: 'Bob', familyName: 'Smith' }],
296
+ emailAddresses: [{ value: 'bob@company.com', type: 'work' }],
297
+ },
298
+ },
299
+ contactGroups: {
300
+ 'contactGroups/myContacts': {
301
+ resourceName: 'contactGroups/myContacts',
302
+ etag: generateEtag(),
303
+ name: 'My Contacts',
304
+ groupType: 'SYSTEM_CONTACT_GROUP',
305
+ memberCount: 2,
306
+ memberResourceNames: ['people/c001', 'people/c002'],
307
+ },
308
+ },
309
+ },
310
+ };
311
+ }
312
+
313
+ export const DEFAULT_USER_EMAIL = DEFAULT_EMAIL;
@@ -0,0 +1,225 @@
1
+ // === Top-level store ===
2
+
3
+ export interface FwsStore {
4
+ gmail: GmailStore;
5
+ calendar: CalendarStore;
6
+ drive: DriveStore;
7
+ tasks: TasksStore;
8
+ sheets: SheetsStore;
9
+ people: PeopleStore;
10
+ }
11
+
12
+ // === Gmail ===
13
+
14
+ export interface GmailStore {
15
+ profile: GmailProfile;
16
+ messages: Record<string, GmailMessage>;
17
+ labels: Record<string, GmailLabel>;
18
+ nextHistoryId: number;
19
+ }
20
+
21
+ export interface GmailProfile {
22
+ emailAddress: string;
23
+ messagesTotal: number;
24
+ threadsTotal: number;
25
+ historyId: string;
26
+ }
27
+
28
+ export interface GmailMessage {
29
+ id: string;
30
+ threadId: string;
31
+ labelIds: string[];
32
+ snippet: string;
33
+ historyId: string;
34
+ internalDate: string;
35
+ sizeEstimate: number;
36
+ raw?: string;
37
+ payload: GmailMessagePart;
38
+ }
39
+
40
+ export interface GmailMessagePart {
41
+ partId: string;
42
+ mimeType: string;
43
+ filename: string;
44
+ headers: Array<{ name: string; value: string }>;
45
+ body: { attachmentId?: string; size: number; data?: string };
46
+ parts?: GmailMessagePart[];
47
+ }
48
+
49
+ export interface GmailLabel {
50
+ id: string;
51
+ name: string;
52
+ type: 'system' | 'user';
53
+ messageListVisibility?: 'show' | 'hide';
54
+ labelListVisibility?: 'labelShow' | 'labelShowIfUnread' | 'labelHide';
55
+ messagesTotal?: number;
56
+ messagesUnread?: number;
57
+ threadsTotal?: number;
58
+ threadsUnread?: number;
59
+ color?: { textColor: string; backgroundColor: string };
60
+ }
61
+
62
+ // === Calendar ===
63
+
64
+ export interface CalendarStore {
65
+ calendars: Record<string, CalendarEntry>;
66
+ events: Record<string, Record<string, CalendarEvent>>;
67
+ calendarList: Record<string, CalendarListEntry>;
68
+ }
69
+
70
+ export interface CalendarEntry {
71
+ kind: 'calendar#calendar';
72
+ id: string;
73
+ summary: string;
74
+ description?: string;
75
+ timeZone?: string;
76
+ etag: string;
77
+ }
78
+
79
+ export interface CalendarListEntry {
80
+ kind: 'calendar#calendarListEntry';
81
+ id: string;
82
+ summary: string;
83
+ description?: string;
84
+ timeZone?: string;
85
+ accessRole: string;
86
+ defaultReminders: unknown[];
87
+ selected?: boolean;
88
+ primary?: boolean;
89
+ etag: string;
90
+ }
91
+
92
+ export interface CalendarEvent {
93
+ kind: 'calendar#event';
94
+ id: string;
95
+ status: 'confirmed' | 'tentative' | 'cancelled';
96
+ summary?: string;
97
+ description?: string;
98
+ location?: string;
99
+ start: { dateTime?: string; date?: string; timeZone?: string };
100
+ end: { dateTime?: string; date?: string; timeZone?: string };
101
+ created: string;
102
+ updated: string;
103
+ creator: { email: string };
104
+ organizer: { email: string; self?: boolean };
105
+ attendees?: Array<{ email: string; responseStatus: string }>;
106
+ etag: string;
107
+ htmlLink: string;
108
+ iCalUID: string;
109
+ }
110
+
111
+ // === Drive ===
112
+
113
+ export interface DriveStore {
114
+ files: Record<string, DriveFile>;
115
+ }
116
+
117
+ export interface DriveFile {
118
+ kind: 'drive#file';
119
+ id: string;
120
+ name: string;
121
+ mimeType: string;
122
+ parents?: string[];
123
+ createdTime: string;
124
+ modifiedTime: string;
125
+ size?: string;
126
+ trashed: boolean;
127
+ starred: boolean;
128
+ owners?: Array<{ emailAddress: string; displayName: string }>;
129
+ webViewLink?: string;
130
+ description?: string;
131
+ }
132
+
133
+ // === Tasks ===
134
+
135
+ export interface TasksStore {
136
+ taskLists: Record<string, TaskList>;
137
+ tasks: Record<string, Record<string, Task>>; // taskListId -> taskId -> task
138
+ }
139
+
140
+ export interface TaskList {
141
+ kind: 'tasks#taskList';
142
+ id: string;
143
+ title: string;
144
+ updated: string;
145
+ selfLink: string;
146
+ }
147
+
148
+ export interface Task {
149
+ kind: 'tasks#task';
150
+ id: string;
151
+ title: string;
152
+ updated: string;
153
+ selfLink: string;
154
+ status: 'needsAction' | 'completed';
155
+ due?: string;
156
+ notes?: string;
157
+ completed?: string;
158
+ parent?: string;
159
+ position: string;
160
+ links?: Array<{ type: string; description: string; link: string }>;
161
+ }
162
+
163
+ // === Sheets ===
164
+
165
+ export interface SheetsStore {
166
+ spreadsheets: Record<string, Spreadsheet>;
167
+ }
168
+
169
+ export interface Spreadsheet {
170
+ spreadsheetId: string;
171
+ properties: {
172
+ title: string;
173
+ locale?: string;
174
+ autoRecalc?: string;
175
+ timeZone?: string;
176
+ };
177
+ sheets: Sheet[];
178
+ spreadsheetUrl: string;
179
+ }
180
+
181
+ export interface Sheet {
182
+ properties: {
183
+ sheetId: number;
184
+ title: string;
185
+ index: number;
186
+ sheetType: string;
187
+ gridProperties: { rowCount: number; columnCount: number };
188
+ };
189
+ data?: Array<{
190
+ startRow?: number;
191
+ startColumn?: number;
192
+ rowData?: Array<{ values?: Array<{ formattedValue?: string; userEnteredValue?: any }> }>;
193
+ }>;
194
+ }
195
+
196
+ // Cell values stored separately for easy access
197
+ export interface SheetValues {
198
+ // key: "spreadsheetId:sheetTitle" -> 2D array of cell values
199
+ [key: string]: string[][];
200
+ }
201
+
202
+ // === People ===
203
+
204
+ export interface PeopleStore {
205
+ contacts: Record<string, Person>;
206
+ contactGroups: Record<string, ContactGroup>;
207
+ }
208
+
209
+ export interface Person {
210
+ resourceName: string;
211
+ etag: string;
212
+ names?: Array<{ displayName: string; familyName?: string; givenName?: string }>;
213
+ emailAddresses?: Array<{ value: string; type?: string }>;
214
+ phoneNumbers?: Array<{ value: string; type?: string }>;
215
+ organizations?: Array<{ name?: string; title?: string }>;
216
+ }
217
+
218
+ export interface ContactGroup {
219
+ resourceName: string;
220
+ etag: string;
221
+ name: string;
222
+ groupType: 'USER_CONTACT_GROUP' | 'SYSTEM_CONTACT_GROUP';
223
+ memberCount: number;
224
+ memberResourceNames?: string[];
225
+ }
package/src/util/id.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { nanoid } from 'nanoid';
2
+
3
+ export function generateId(length = 16): string {
4
+ return nanoid(length);
5
+ }
6
+
7
+ export function generateEtag(): string {
8
+ return `"${nanoid(12)}"`;
9
+ }