@hasna/calendar 0.1.7 → 0.1.9
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/dist/cli/index.js +96 -18
- package/dist/db/attendees.d.ts.map +1 -1
- package/dist/db/event-time.d.ts +9 -0
- package/dist/db/event-time.d.ts.map +1 -0
- package/dist/db/events.d.ts.map +1 -1
- package/dist/index.js +103 -25
- package/dist/mcp/index.js +99 -18
- package/dist/server/index.js +99 -18
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -3241,6 +3241,82 @@ function deleteCalendar(id, db) {
|
|
|
3241
3241
|
return result.changes > 0;
|
|
3242
3242
|
}
|
|
3243
3243
|
|
|
3244
|
+
// src/db/event-time.ts
|
|
3245
|
+
var ISO_DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?(Z|[+-]\d{2}:\d{2})$/;
|
|
3246
|
+
var NS_PER_MS = 1000000n;
|
|
3247
|
+
var NS_PER_MINUTE = 60000000000n;
|
|
3248
|
+
function daysInMonth(year, month) {
|
|
3249
|
+
const date = new Date(0);
|
|
3250
|
+
date.setUTCFullYear(year, month, 0);
|
|
3251
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
3252
|
+
return date.getUTCDate();
|
|
3253
|
+
}
|
|
3254
|
+
function timestampBaseMs(year, month, day, hour, minute, second) {
|
|
3255
|
+
const date = new Date(0);
|
|
3256
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
3257
|
+
date.setUTCHours(hour, minute, second, 0);
|
|
3258
|
+
return date.getTime();
|
|
3259
|
+
}
|
|
3260
|
+
function parseEventTimestamp(value) {
|
|
3261
|
+
const match = ISO_DATE_TIME_RE.exec(value);
|
|
3262
|
+
if (!match) {
|
|
3263
|
+
throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
|
|
3264
|
+
}
|
|
3265
|
+
const year = Number(match[1]);
|
|
3266
|
+
const month = Number(match[2]);
|
|
3267
|
+
const day = Number(match[3]);
|
|
3268
|
+
const hour = Number(match[4]);
|
|
3269
|
+
const minute = Number(match[5]);
|
|
3270
|
+
const second = Number(match[6]);
|
|
3271
|
+
const fraction = match[7] || "";
|
|
3272
|
+
const offset = match[8];
|
|
3273
|
+
const maxDay = month >= 1 && month <= 12 ? daysInMonth(year, month) : 0;
|
|
3274
|
+
if (month < 1 || month > 12 || day < 1 || day > maxDay || hour > 23 || minute > 59 || second > 59) {
|
|
3275
|
+
throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
|
|
3276
|
+
}
|
|
3277
|
+
let offsetMinutes = 0;
|
|
3278
|
+
if (offset !== "Z") {
|
|
3279
|
+
const offsetSign = offset[0] === "-" ? -1 : 1;
|
|
3280
|
+
const offsetHour = Number(offset.slice(1, 3));
|
|
3281
|
+
const offsetMinute = Number(offset.slice(4, 6));
|
|
3282
|
+
if (offsetHour > 23 || offsetMinute > 59) {
|
|
3283
|
+
throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
|
|
3284
|
+
}
|
|
3285
|
+
offsetMinutes = offsetSign * (offsetHour * 60 + offsetMinute);
|
|
3286
|
+
}
|
|
3287
|
+
const timestamp = timestampBaseMs(year, month, day, hour, minute, second);
|
|
3288
|
+
if (!Number.isFinite(timestamp)) {
|
|
3289
|
+
throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
|
|
3290
|
+
}
|
|
3291
|
+
const fractionalNs = BigInt(fraction.padEnd(9, "0") || "0");
|
|
3292
|
+
return BigInt(timestamp) * NS_PER_MS + fractionalNs - BigInt(offsetMinutes) * NS_PER_MINUTE;
|
|
3293
|
+
}
|
|
3294
|
+
function assertEventEndsAfterStart(startAt, endAt) {
|
|
3295
|
+
const start = parseEventTimestamp(startAt);
|
|
3296
|
+
const end = parseEventTimestamp(endAt);
|
|
3297
|
+
if (end <= start) {
|
|
3298
|
+
throw new RangeError("Event end_at must be after start_at");
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
function parseTimeRange(startAt, endAt) {
|
|
3302
|
+
const start = parseEventTimestamp(startAt);
|
|
3303
|
+
const end = parseEventTimestamp(endAt);
|
|
3304
|
+
if (end <= start) {
|
|
3305
|
+
throw new RangeError("Time range end must be after start");
|
|
3306
|
+
}
|
|
3307
|
+
return { start, end };
|
|
3308
|
+
}
|
|
3309
|
+
function compareEventInstants(a, b) {
|
|
3310
|
+
if (a < b)
|
|
3311
|
+
return -1;
|
|
3312
|
+
if (a > b)
|
|
3313
|
+
return 1;
|
|
3314
|
+
return 0;
|
|
3315
|
+
}
|
|
3316
|
+
function compareEventTimestampStrings(a, b) {
|
|
3317
|
+
return compareEventInstants(parseEventTimestamp(a), parseEventTimestamp(b)) || a.localeCompare(b);
|
|
3318
|
+
}
|
|
3319
|
+
|
|
3244
3320
|
// src/db/events.ts
|
|
3245
3321
|
function rowToEvent(row) {
|
|
3246
3322
|
return {
|
|
@@ -3266,9 +3342,13 @@ function rowToEvent(row) {
|
|
|
3266
3342
|
updated_at: row.updated_at
|
|
3267
3343
|
};
|
|
3268
3344
|
}
|
|
3345
|
+
function positiveInteger(value) {
|
|
3346
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
|
|
3347
|
+
}
|
|
3269
3348
|
function createEvent2(input, db) {
|
|
3270
3349
|
db = db || getDatabase();
|
|
3271
3350
|
const id = crypto.randomUUID().slice(0, 8);
|
|
3351
|
+
assertEventEndsAfterStart(input.start_at, input.end_at);
|
|
3272
3352
|
db.run(`INSERT INTO events (id, calendar_id, org_id, title, description, location, start_at, end_at, all_day, timezone, status, busy_type, visibility, recurrence_rule, recurrence_exception_dates, source_task_id, created_by, metadata)
|
|
3273
3353
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.calendar_id, input.org_id, input.title, input.description || null, input.location || null, input.start_at, input.end_at, input.all_day ? 1 : 0, input.timezone || "UTC", input.status || "confirmed", input.busy_type || "busy", input.visibility || "default", input.recurrence_rule || null, input.recurrence_exception_dates ? JSON.stringify(input.recurrence_exception_dates) : null, input.source_task_id || null, input.created_by || null, JSON.stringify(input.metadata || {})]);
|
|
3274
3354
|
return getEvent(id, db);
|
|
@@ -3282,6 +3362,8 @@ function listEvents(filter = {}, db) {
|
|
|
3282
3362
|
db = db || getDatabase();
|
|
3283
3363
|
const conditions = [];
|
|
3284
3364
|
const params = [];
|
|
3365
|
+
const after = filter.after ? parseEventTimestamp(filter.after) : null;
|
|
3366
|
+
const before = filter.before ? parseEventTimestamp(filter.before) : null;
|
|
3285
3367
|
if (filter.calendar_id) {
|
|
3286
3368
|
conditions.push("calendar_id = ?");
|
|
3287
3369
|
params.push(filter.calendar_id);
|
|
@@ -3294,14 +3376,6 @@ function listEvents(filter = {}, db) {
|
|
|
3294
3376
|
conditions.push("status = ?");
|
|
3295
3377
|
params.push(filter.status);
|
|
3296
3378
|
}
|
|
3297
|
-
if (filter.after) {
|
|
3298
|
-
conditions.push("start_at >= ?");
|
|
3299
|
-
params.push(filter.after);
|
|
3300
|
-
}
|
|
3301
|
-
if (filter.before) {
|
|
3302
|
-
conditions.push("start_at <= ?");
|
|
3303
|
-
params.push(filter.before);
|
|
3304
|
-
}
|
|
3305
3379
|
if (filter.created_by) {
|
|
3306
3380
|
conditions.push("created_by = ?");
|
|
3307
3381
|
params.push(filter.created_by);
|
|
@@ -3311,16 +3385,20 @@ function listEvents(filter = {}, db) {
|
|
|
3311
3385
|
params.push(filter.source_task_id);
|
|
3312
3386
|
}
|
|
3313
3387
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
3314
|
-
const
|
|
3315
|
-
const
|
|
3316
|
-
const
|
|
3317
|
-
|
|
3388
|
+
const rows = db.query(`SELECT * FROM events ${where}`).all(...params);
|
|
3389
|
+
const events = rows.map(rowToEvent).map((event) => ({ event, start: parseEventTimestamp(event.start_at) })).filter(({ start }) => (after === null || start >= after) && (before === null || start <= before)).sort((a, b) => compareEventInstants(a.start, b.start) || a.event.start_at.localeCompare(b.event.start_at)).map(({ event }) => event);
|
|
3390
|
+
const offset = positiveInteger(filter.offset) || 0;
|
|
3391
|
+
const limit = positiveInteger(filter.limit);
|
|
3392
|
+
return limit ? events.slice(offset, offset + limit) : events.slice(offset);
|
|
3318
3393
|
}
|
|
3319
3394
|
function updateEvent(id, input, db) {
|
|
3320
3395
|
db = db || getDatabase();
|
|
3321
3396
|
const existing = getEvent(id, db);
|
|
3322
3397
|
if (!existing)
|
|
3323
3398
|
throw new NotFoundError("Event", id);
|
|
3399
|
+
const startAt = input.start_at ?? existing.start_at;
|
|
3400
|
+
const endAt = input.end_at ?? existing.end_at;
|
|
3401
|
+
assertEventEndsAfterStart(startAt, endAt);
|
|
3324
3402
|
db.run(`UPDATE events SET title = ?, description = ?, location = ?, start_at = ?, end_at = ?, all_day = ?, timezone = ?, status = ?, busy_type = ?, visibility = ?, recurrence_rule = ?, recurrence_exception_dates = ?, source_task_id = ?, metadata = ?, updated_at = datetime('now') WHERE id = ?`, [input.title ?? existing.title, input.description !== undefined ? input.description : existing.description, input.location !== undefined ? input.location : existing.location, input.start_at ?? existing.start_at, input.end_at ?? existing.end_at, input.all_day !== undefined ? input.all_day ? 1 : 0 : existing.all_day ? 1 : 0, input.timezone ?? existing.timezone, input.status ?? existing.status, input.busy_type ?? existing.busy_type, input.visibility ?? existing.visibility, input.recurrence_rule !== undefined ? input.recurrence_rule : existing.recurrence_rule, input.recurrence_exception_dates !== undefined ? input.recurrence_exception_dates ? JSON.stringify(input.recurrence_exception_dates) : null : existing.recurrence_exception_dates ? JSON.stringify(existing.recurrence_exception_dates) : null, input.source_task_id !== undefined ? input.source_task_id : existing.source_task_id, JSON.stringify(input.metadata ?? existing.metadata), id]);
|
|
3325
3403
|
return getEvent(id, db);
|
|
3326
3404
|
}
|
|
@@ -3332,18 +3410,18 @@ function deleteEvent(id, db) {
|
|
|
3332
3410
|
function findConflicts(calendarId, range, excludeEventId, db) {
|
|
3333
3411
|
db = db || getDatabase();
|
|
3334
3412
|
const exclude = excludeEventId ? "AND id != ?" : "";
|
|
3335
|
-
const params = excludeEventId ? [calendarId,
|
|
3336
|
-
const
|
|
3337
|
-
|
|
3413
|
+
const params = excludeEventId ? [calendarId, excludeEventId] : [calendarId];
|
|
3414
|
+
const { start: rangeStart, end: rangeEnd } = parseTimeRange(range.start, range.end);
|
|
3415
|
+
const rows = db.query(`SELECT * FROM events WHERE calendar_id = ? AND status != 'cancelled' ${exclude}`).all(...params);
|
|
3416
|
+
return rows.map(rowToEvent).map((event) => ({ event, start: parseEventTimestamp(event.start_at), end: parseEventTimestamp(event.end_at) })).filter(({ start, end }) => start < rangeEnd && end > rangeStart).sort((a, b) => compareEventInstants(a.start, b.start) || a.event.start_at.localeCompare(b.event.start_at)).map(({ event }) => event);
|
|
3338
3417
|
}
|
|
3339
3418
|
function searchEvents(query, orgId, db) {
|
|
3340
3419
|
db = db || getDatabase();
|
|
3341
3420
|
const rows = db.query(`SELECT e.* FROM events e
|
|
3342
3421
|
INNER JOIN events_fts f ON f.rowid = e.rowid
|
|
3343
3422
|
WHERE events_fts MATCH ?
|
|
3344
|
-
${orgId ? "AND e.org_id = ?" : ""}
|
|
3345
|
-
|
|
3346
|
-
return rows.map(rowToEvent);
|
|
3423
|
+
${orgId ? "AND e.org_id = ?" : ""}`).all(query, ...orgId ? [orgId] : []);
|
|
3424
|
+
return rows.map(rowToEvent).sort((a, b) => compareEventTimestampStrings(a.start_at, b.start_at));
|
|
3347
3425
|
}
|
|
3348
3426
|
|
|
3349
3427
|
// src/db/attendees.ts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attendees.d.ts","sourceRoot":"","sources":["../../src/db/attendees.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"attendees.d.ts","sourceRoot":"","sources":["../../src/db/attendees.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGtC,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAkBjG,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,aAAa,CAUvF;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,aAAa,GAAG,IAAI,CAI3E;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,aAAa,EAAE,CAIpF;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,aAAa,EAAE,CAUjF;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,aAAa,CAYnG;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAIjE;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAIhF"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function parseEventTimestamp(value: string): bigint;
|
|
2
|
+
export declare function assertEventEndsAfterStart(startAt: string, endAt: string): void;
|
|
3
|
+
export declare function parseTimeRange(startAt: string, endAt: string): {
|
|
4
|
+
start: bigint;
|
|
5
|
+
end: bigint;
|
|
6
|
+
};
|
|
7
|
+
export declare function compareEventInstants(a: bigint, b: bigint): number;
|
|
8
|
+
export declare function compareEventTimestampStrings(a: string, b: string): number;
|
|
9
|
+
//# sourceMappingURL=event-time.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-time.d.ts","sourceRoot":"","sources":["../../src/db/event-time.ts"],"names":[],"mappings":"AAkBA,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAsCzD;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAO9E;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAS7F;AAED,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAIjE;AAED,wBAAgB,4BAA4B,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAEzE"}
|
package/dist/db/events.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/db/events.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/db/events.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGtC,OAAO,KAAK,EAAE,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAgCnF,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,CAYzE;AAED,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,IAAI,CAIhE;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,UAAU,CAAC,MAAM,GAAE,gBAAqB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,EAAE,CAyBhF;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,CAcrF;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAI9D;AAID,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,uEAAuE;AACvE,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,cAAc,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,EAAE,CAiBnH;AAED,0FAA0F;AAC1F,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,cAAc,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,EAAE,CAkBrH;AAID,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,EAAE,CAYlF"}
|
package/dist/index.js
CHANGED
|
@@ -518,6 +518,82 @@ function deleteCalendar(id, db) {
|
|
|
518
518
|
const result = db.run(`DELETE FROM calendars WHERE id = ?`, [id]);
|
|
519
519
|
return result.changes > 0;
|
|
520
520
|
}
|
|
521
|
+
// src/db/event-time.ts
|
|
522
|
+
var ISO_DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?(Z|[+-]\d{2}:\d{2})$/;
|
|
523
|
+
var NS_PER_MS = 1000000n;
|
|
524
|
+
var NS_PER_MINUTE = 60000000000n;
|
|
525
|
+
function daysInMonth(year, month) {
|
|
526
|
+
const date = new Date(0);
|
|
527
|
+
date.setUTCFullYear(year, month, 0);
|
|
528
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
529
|
+
return date.getUTCDate();
|
|
530
|
+
}
|
|
531
|
+
function timestampBaseMs(year, month, day, hour, minute, second) {
|
|
532
|
+
const date = new Date(0);
|
|
533
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
534
|
+
date.setUTCHours(hour, minute, second, 0);
|
|
535
|
+
return date.getTime();
|
|
536
|
+
}
|
|
537
|
+
function parseEventTimestamp(value) {
|
|
538
|
+
const match = ISO_DATE_TIME_RE.exec(value);
|
|
539
|
+
if (!match) {
|
|
540
|
+
throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
|
|
541
|
+
}
|
|
542
|
+
const year = Number(match[1]);
|
|
543
|
+
const month = Number(match[2]);
|
|
544
|
+
const day = Number(match[3]);
|
|
545
|
+
const hour = Number(match[4]);
|
|
546
|
+
const minute = Number(match[5]);
|
|
547
|
+
const second = Number(match[6]);
|
|
548
|
+
const fraction = match[7] || "";
|
|
549
|
+
const offset = match[8];
|
|
550
|
+
const maxDay = month >= 1 && month <= 12 ? daysInMonth(year, month) : 0;
|
|
551
|
+
if (month < 1 || month > 12 || day < 1 || day > maxDay || hour > 23 || minute > 59 || second > 59) {
|
|
552
|
+
throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
|
|
553
|
+
}
|
|
554
|
+
let offsetMinutes = 0;
|
|
555
|
+
if (offset !== "Z") {
|
|
556
|
+
const offsetSign = offset[0] === "-" ? -1 : 1;
|
|
557
|
+
const offsetHour = Number(offset.slice(1, 3));
|
|
558
|
+
const offsetMinute = Number(offset.slice(4, 6));
|
|
559
|
+
if (offsetHour > 23 || offsetMinute > 59) {
|
|
560
|
+
throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
|
|
561
|
+
}
|
|
562
|
+
offsetMinutes = offsetSign * (offsetHour * 60 + offsetMinute);
|
|
563
|
+
}
|
|
564
|
+
const timestamp = timestampBaseMs(year, month, day, hour, minute, second);
|
|
565
|
+
if (!Number.isFinite(timestamp)) {
|
|
566
|
+
throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
|
|
567
|
+
}
|
|
568
|
+
const fractionalNs = BigInt(fraction.padEnd(9, "0") || "0");
|
|
569
|
+
return BigInt(timestamp) * NS_PER_MS + fractionalNs - BigInt(offsetMinutes) * NS_PER_MINUTE;
|
|
570
|
+
}
|
|
571
|
+
function assertEventEndsAfterStart(startAt, endAt) {
|
|
572
|
+
const start = parseEventTimestamp(startAt);
|
|
573
|
+
const end = parseEventTimestamp(endAt);
|
|
574
|
+
if (end <= start) {
|
|
575
|
+
throw new RangeError("Event end_at must be after start_at");
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function parseTimeRange(startAt, endAt) {
|
|
579
|
+
const start = parseEventTimestamp(startAt);
|
|
580
|
+
const end = parseEventTimestamp(endAt);
|
|
581
|
+
if (end <= start) {
|
|
582
|
+
throw new RangeError("Time range end must be after start");
|
|
583
|
+
}
|
|
584
|
+
return { start, end };
|
|
585
|
+
}
|
|
586
|
+
function compareEventInstants(a, b) {
|
|
587
|
+
if (a < b)
|
|
588
|
+
return -1;
|
|
589
|
+
if (a > b)
|
|
590
|
+
return 1;
|
|
591
|
+
return 0;
|
|
592
|
+
}
|
|
593
|
+
function compareEventTimestampStrings(a, b) {
|
|
594
|
+
return compareEventInstants(parseEventTimestamp(a), parseEventTimestamp(b)) || a.localeCompare(b);
|
|
595
|
+
}
|
|
596
|
+
|
|
521
597
|
// src/db/events.ts
|
|
522
598
|
function rowToEvent(row) {
|
|
523
599
|
return {
|
|
@@ -543,9 +619,13 @@ function rowToEvent(row) {
|
|
|
543
619
|
updated_at: row.updated_at
|
|
544
620
|
};
|
|
545
621
|
}
|
|
622
|
+
function positiveInteger(value) {
|
|
623
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
|
|
624
|
+
}
|
|
546
625
|
function createEvent(input, db) {
|
|
547
626
|
db = db || getDatabase();
|
|
548
627
|
const id = crypto.randomUUID().slice(0, 8);
|
|
628
|
+
assertEventEndsAfterStart(input.start_at, input.end_at);
|
|
549
629
|
db.run(`INSERT INTO events (id, calendar_id, org_id, title, description, location, start_at, end_at, all_day, timezone, status, busy_type, visibility, recurrence_rule, recurrence_exception_dates, source_task_id, created_by, metadata)
|
|
550
630
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.calendar_id, input.org_id, input.title, input.description || null, input.location || null, input.start_at, input.end_at, input.all_day ? 1 : 0, input.timezone || "UTC", input.status || "confirmed", input.busy_type || "busy", input.visibility || "default", input.recurrence_rule || null, input.recurrence_exception_dates ? JSON.stringify(input.recurrence_exception_dates) : null, input.source_task_id || null, input.created_by || null, JSON.stringify(input.metadata || {})]);
|
|
551
631
|
return getEvent(id, db);
|
|
@@ -559,6 +639,8 @@ function listEvents(filter = {}, db) {
|
|
|
559
639
|
db = db || getDatabase();
|
|
560
640
|
const conditions = [];
|
|
561
641
|
const params = [];
|
|
642
|
+
const after = filter.after ? parseEventTimestamp(filter.after) : null;
|
|
643
|
+
const before = filter.before ? parseEventTimestamp(filter.before) : null;
|
|
562
644
|
if (filter.calendar_id) {
|
|
563
645
|
conditions.push("calendar_id = ?");
|
|
564
646
|
params.push(filter.calendar_id);
|
|
@@ -571,14 +653,6 @@ function listEvents(filter = {}, db) {
|
|
|
571
653
|
conditions.push("status = ?");
|
|
572
654
|
params.push(filter.status);
|
|
573
655
|
}
|
|
574
|
-
if (filter.after) {
|
|
575
|
-
conditions.push("start_at >= ?");
|
|
576
|
-
params.push(filter.after);
|
|
577
|
-
}
|
|
578
|
-
if (filter.before) {
|
|
579
|
-
conditions.push("start_at <= ?");
|
|
580
|
-
params.push(filter.before);
|
|
581
|
-
}
|
|
582
656
|
if (filter.created_by) {
|
|
583
657
|
conditions.push("created_by = ?");
|
|
584
658
|
params.push(filter.created_by);
|
|
@@ -588,16 +662,20 @@ function listEvents(filter = {}, db) {
|
|
|
588
662
|
params.push(filter.source_task_id);
|
|
589
663
|
}
|
|
590
664
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
591
|
-
const
|
|
592
|
-
const
|
|
593
|
-
const
|
|
594
|
-
|
|
665
|
+
const rows = db.query(`SELECT * FROM events ${where}`).all(...params);
|
|
666
|
+
const events = rows.map(rowToEvent).map((event) => ({ event, start: parseEventTimestamp(event.start_at) })).filter(({ start }) => (after === null || start >= after) && (before === null || start <= before)).sort((a, b) => compareEventInstants(a.start, b.start) || a.event.start_at.localeCompare(b.event.start_at)).map(({ event }) => event);
|
|
667
|
+
const offset = positiveInteger(filter.offset) || 0;
|
|
668
|
+
const limit = positiveInteger(filter.limit);
|
|
669
|
+
return limit ? events.slice(offset, offset + limit) : events.slice(offset);
|
|
595
670
|
}
|
|
596
671
|
function updateEvent(id, input, db) {
|
|
597
672
|
db = db || getDatabase();
|
|
598
673
|
const existing = getEvent(id, db);
|
|
599
674
|
if (!existing)
|
|
600
675
|
throw new NotFoundError("Event", id);
|
|
676
|
+
const startAt = input.start_at ?? existing.start_at;
|
|
677
|
+
const endAt = input.end_at ?? existing.end_at;
|
|
678
|
+
assertEventEndsAfterStart(startAt, endAt);
|
|
601
679
|
db.run(`UPDATE events SET title = ?, description = ?, location = ?, start_at = ?, end_at = ?, all_day = ?, timezone = ?, status = ?, busy_type = ?, visibility = ?, recurrence_rule = ?, recurrence_exception_dates = ?, source_task_id = ?, metadata = ?, updated_at = datetime('now') WHERE id = ?`, [input.title ?? existing.title, input.description !== undefined ? input.description : existing.description, input.location !== undefined ? input.location : existing.location, input.start_at ?? existing.start_at, input.end_at ?? existing.end_at, input.all_day !== undefined ? input.all_day ? 1 : 0 : existing.all_day ? 1 : 0, input.timezone ?? existing.timezone, input.status ?? existing.status, input.busy_type ?? existing.busy_type, input.visibility ?? existing.visibility, input.recurrence_rule !== undefined ? input.recurrence_rule : existing.recurrence_rule, input.recurrence_exception_dates !== undefined ? input.recurrence_exception_dates ? JSON.stringify(input.recurrence_exception_dates) : null : existing.recurrence_exception_dates ? JSON.stringify(existing.recurrence_exception_dates) : null, input.source_task_id !== undefined ? input.source_task_id : existing.source_task_id, JSON.stringify(input.metadata ?? existing.metadata), id]);
|
|
602
680
|
return getEvent(id, db);
|
|
603
681
|
}
|
|
@@ -609,28 +687,28 @@ function deleteEvent(id, db) {
|
|
|
609
687
|
function findConflicts(calendarId, range, excludeEventId, db) {
|
|
610
688
|
db = db || getDatabase();
|
|
611
689
|
const exclude = excludeEventId ? "AND id != ?" : "";
|
|
612
|
-
const params = excludeEventId ? [calendarId,
|
|
613
|
-
const
|
|
614
|
-
|
|
690
|
+
const params = excludeEventId ? [calendarId, excludeEventId] : [calendarId];
|
|
691
|
+
const { start: rangeStart, end: rangeEnd } = parseTimeRange(range.start, range.end);
|
|
692
|
+
const rows = db.query(`SELECT * FROM events WHERE calendar_id = ? AND status != 'cancelled' ${exclude}`).all(...params);
|
|
693
|
+
return rows.map(rowToEvent).map((event) => ({ event, start: parseEventTimestamp(event.start_at), end: parseEventTimestamp(event.end_at) })).filter(({ start, end }) => start < rangeEnd && end > rangeStart).sort((a, b) => compareEventInstants(a.start, b.start) || a.event.start_at.localeCompare(b.event.start_at)).map(({ event }) => event);
|
|
615
694
|
}
|
|
616
695
|
function findAgentConflicts(agentId, range, excludeEventId, db) {
|
|
617
696
|
db = db || getDatabase();
|
|
618
697
|
const exclude = excludeEventId ? "AND e.id != ?" : "";
|
|
619
|
-
const params = excludeEventId ? [agentId,
|
|
698
|
+
const params = excludeEventId ? [agentId, excludeEventId] : [agentId];
|
|
699
|
+
const { start: rangeStart, end: rangeEnd } = parseTimeRange(range.start, range.end);
|
|
620
700
|
const rows = db.query(`SELECT e.* FROM events e
|
|
621
701
|
INNER JOIN event_attendees a ON a.event_id = e.id
|
|
622
|
-
WHERE a.agent_id = ? AND e.
|
|
623
|
-
|
|
624
|
-
return rows.map(rowToEvent);
|
|
702
|
+
WHERE a.agent_id = ? AND e.status != 'cancelled' ${exclude}`).all(...params);
|
|
703
|
+
return rows.map(rowToEvent).map((event) => ({ event, start: parseEventTimestamp(event.start_at), end: parseEventTimestamp(event.end_at) })).filter(({ start, end }) => start < rangeEnd && end > rangeStart).sort((a, b) => compareEventInstants(a.start, b.start) || a.event.start_at.localeCompare(b.event.start_at)).map(({ event }) => event);
|
|
625
704
|
}
|
|
626
705
|
function searchEvents(query, orgId, db) {
|
|
627
706
|
db = db || getDatabase();
|
|
628
707
|
const rows = db.query(`SELECT e.* FROM events e
|
|
629
708
|
INNER JOIN events_fts f ON f.rowid = e.rowid
|
|
630
709
|
WHERE events_fts MATCH ?
|
|
631
|
-
${orgId ? "AND e.org_id = ?" : ""}
|
|
632
|
-
|
|
633
|
-
return rows.map(rowToEvent);
|
|
710
|
+
${orgId ? "AND e.org_id = ?" : ""}`).all(query, ...orgId ? [orgId] : []);
|
|
711
|
+
return rows.map(rowToEvent).sort((a, b) => compareEventTimestampStrings(a.start_at, b.start_at));
|
|
634
712
|
}
|
|
635
713
|
// src/db/attendees.ts
|
|
636
714
|
function rowToAttendee(row) {
|
|
@@ -665,10 +743,10 @@ function getAttendeesForEvent(eventId, db) {
|
|
|
665
743
|
}
|
|
666
744
|
function getEventsForAgent(agentId, db) {
|
|
667
745
|
db = db || getDatabase();
|
|
668
|
-
const rows = db.query(`SELECT a
|
|
746
|
+
const rows = db.query(`SELECT a.*, e.start_at AS event_start_at FROM event_attendees a
|
|
669
747
|
INNER JOIN events e ON e.id = a.event_id
|
|
670
|
-
WHERE a.agent_id = ? AND e.status != 'cancelled'
|
|
671
|
-
|
|
748
|
+
WHERE a.agent_id = ? AND e.status != 'cancelled'`).all(agentId);
|
|
749
|
+
rows.sort((a, b) => compareEventTimestampStrings(a.event_start_at, b.event_start_at));
|
|
672
750
|
return rows.map(rowToAttendee);
|
|
673
751
|
}
|
|
674
752
|
function updateAttendee(id, input, db) {
|
package/dist/mcp/index.js
CHANGED
|
@@ -4423,6 +4423,83 @@ var init_calendars = __esm(() => {
|
|
|
4423
4423
|
init_types2();
|
|
4424
4424
|
});
|
|
4425
4425
|
|
|
4426
|
+
// src/db/event-time.ts
|
|
4427
|
+
function daysInMonth(year, month) {
|
|
4428
|
+
const date = new Date(0);
|
|
4429
|
+
date.setUTCFullYear(year, month, 0);
|
|
4430
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
4431
|
+
return date.getUTCDate();
|
|
4432
|
+
}
|
|
4433
|
+
function timestampBaseMs(year, month, day, hour, minute, second) {
|
|
4434
|
+
const date = new Date(0);
|
|
4435
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
4436
|
+
date.setUTCHours(hour, minute, second, 0);
|
|
4437
|
+
return date.getTime();
|
|
4438
|
+
}
|
|
4439
|
+
function parseEventTimestamp(value) {
|
|
4440
|
+
const match = ISO_DATE_TIME_RE.exec(value);
|
|
4441
|
+
if (!match) {
|
|
4442
|
+
throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
|
|
4443
|
+
}
|
|
4444
|
+
const year = Number(match[1]);
|
|
4445
|
+
const month = Number(match[2]);
|
|
4446
|
+
const day = Number(match[3]);
|
|
4447
|
+
const hour = Number(match[4]);
|
|
4448
|
+
const minute = Number(match[5]);
|
|
4449
|
+
const second = Number(match[6]);
|
|
4450
|
+
const fraction = match[7] || "";
|
|
4451
|
+
const offset = match[8];
|
|
4452
|
+
const maxDay = month >= 1 && month <= 12 ? daysInMonth(year, month) : 0;
|
|
4453
|
+
if (month < 1 || month > 12 || day < 1 || day > maxDay || hour > 23 || minute > 59 || second > 59) {
|
|
4454
|
+
throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
|
|
4455
|
+
}
|
|
4456
|
+
let offsetMinutes = 0;
|
|
4457
|
+
if (offset !== "Z") {
|
|
4458
|
+
const offsetSign = offset[0] === "-" ? -1 : 1;
|
|
4459
|
+
const offsetHour = Number(offset.slice(1, 3));
|
|
4460
|
+
const offsetMinute = Number(offset.slice(4, 6));
|
|
4461
|
+
if (offsetHour > 23 || offsetMinute > 59) {
|
|
4462
|
+
throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
|
|
4463
|
+
}
|
|
4464
|
+
offsetMinutes = offsetSign * (offsetHour * 60 + offsetMinute);
|
|
4465
|
+
}
|
|
4466
|
+
const timestamp = timestampBaseMs(year, month, day, hour, minute, second);
|
|
4467
|
+
if (!Number.isFinite(timestamp)) {
|
|
4468
|
+
throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
|
|
4469
|
+
}
|
|
4470
|
+
const fractionalNs = BigInt(fraction.padEnd(9, "0") || "0");
|
|
4471
|
+
return BigInt(timestamp) * NS_PER_MS + fractionalNs - BigInt(offsetMinutes) * NS_PER_MINUTE;
|
|
4472
|
+
}
|
|
4473
|
+
function assertEventEndsAfterStart(startAt, endAt) {
|
|
4474
|
+
const start = parseEventTimestamp(startAt);
|
|
4475
|
+
const end = parseEventTimestamp(endAt);
|
|
4476
|
+
if (end <= start) {
|
|
4477
|
+
throw new RangeError("Event end_at must be after start_at");
|
|
4478
|
+
}
|
|
4479
|
+
}
|
|
4480
|
+
function parseTimeRange(startAt, endAt) {
|
|
4481
|
+
const start = parseEventTimestamp(startAt);
|
|
4482
|
+
const end = parseEventTimestamp(endAt);
|
|
4483
|
+
if (end <= start) {
|
|
4484
|
+
throw new RangeError("Time range end must be after start");
|
|
4485
|
+
}
|
|
4486
|
+
return { start, end };
|
|
4487
|
+
}
|
|
4488
|
+
function compareEventInstants(a, b) {
|
|
4489
|
+
if (a < b)
|
|
4490
|
+
return -1;
|
|
4491
|
+
if (a > b)
|
|
4492
|
+
return 1;
|
|
4493
|
+
return 0;
|
|
4494
|
+
}
|
|
4495
|
+
function compareEventTimestampStrings(a, b) {
|
|
4496
|
+
return compareEventInstants(parseEventTimestamp(a), parseEventTimestamp(b)) || a.localeCompare(b);
|
|
4497
|
+
}
|
|
4498
|
+
var ISO_DATE_TIME_RE, NS_PER_MS = 1000000n, NS_PER_MINUTE = 60000000000n;
|
|
4499
|
+
var init_event_time = __esm(() => {
|
|
4500
|
+
ISO_DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?(Z|[+-]\d{2}:\d{2})$/;
|
|
4501
|
+
});
|
|
4502
|
+
|
|
4426
4503
|
// src/db/events.ts
|
|
4427
4504
|
function rowToEvent(row) {
|
|
4428
4505
|
return {
|
|
@@ -4448,9 +4525,13 @@ function rowToEvent(row) {
|
|
|
4448
4525
|
updated_at: row.updated_at
|
|
4449
4526
|
};
|
|
4450
4527
|
}
|
|
4528
|
+
function positiveInteger(value) {
|
|
4529
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
|
|
4530
|
+
}
|
|
4451
4531
|
function createEvent(input, db) {
|
|
4452
4532
|
db = db || getDatabase();
|
|
4453
4533
|
const id = crypto.randomUUID().slice(0, 8);
|
|
4534
|
+
assertEventEndsAfterStart(input.start_at, input.end_at);
|
|
4454
4535
|
db.run(`INSERT INTO events (id, calendar_id, org_id, title, description, location, start_at, end_at, all_day, timezone, status, busy_type, visibility, recurrence_rule, recurrence_exception_dates, source_task_id, created_by, metadata)
|
|
4455
4536
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.calendar_id, input.org_id, input.title, input.description || null, input.location || null, input.start_at, input.end_at, input.all_day ? 1 : 0, input.timezone || "UTC", input.status || "confirmed", input.busy_type || "busy", input.visibility || "default", input.recurrence_rule || null, input.recurrence_exception_dates ? JSON.stringify(input.recurrence_exception_dates) : null, input.source_task_id || null, input.created_by || null, JSON.stringify(input.metadata || {})]);
|
|
4456
4537
|
return getEvent(id, db);
|
|
@@ -4464,6 +4545,8 @@ function listEvents(filter = {}, db) {
|
|
|
4464
4545
|
db = db || getDatabase();
|
|
4465
4546
|
const conditions = [];
|
|
4466
4547
|
const params = [];
|
|
4548
|
+
const after = filter.after ? parseEventTimestamp(filter.after) : null;
|
|
4549
|
+
const before = filter.before ? parseEventTimestamp(filter.before) : null;
|
|
4467
4550
|
if (filter.calendar_id) {
|
|
4468
4551
|
conditions.push("calendar_id = ?");
|
|
4469
4552
|
params.push(filter.calendar_id);
|
|
@@ -4476,14 +4559,6 @@ function listEvents(filter = {}, db) {
|
|
|
4476
4559
|
conditions.push("status = ?");
|
|
4477
4560
|
params.push(filter.status);
|
|
4478
4561
|
}
|
|
4479
|
-
if (filter.after) {
|
|
4480
|
-
conditions.push("start_at >= ?");
|
|
4481
|
-
params.push(filter.after);
|
|
4482
|
-
}
|
|
4483
|
-
if (filter.before) {
|
|
4484
|
-
conditions.push("start_at <= ?");
|
|
4485
|
-
params.push(filter.before);
|
|
4486
|
-
}
|
|
4487
4562
|
if (filter.created_by) {
|
|
4488
4563
|
conditions.push("created_by = ?");
|
|
4489
4564
|
params.push(filter.created_by);
|
|
@@ -4493,16 +4568,20 @@ function listEvents(filter = {}, db) {
|
|
|
4493
4568
|
params.push(filter.source_task_id);
|
|
4494
4569
|
}
|
|
4495
4570
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
4496
|
-
const
|
|
4497
|
-
const
|
|
4498
|
-
const
|
|
4499
|
-
|
|
4571
|
+
const rows = db.query(`SELECT * FROM events ${where}`).all(...params);
|
|
4572
|
+
const events = rows.map(rowToEvent).map((event) => ({ event, start: parseEventTimestamp(event.start_at) })).filter(({ start }) => (after === null || start >= after) && (before === null || start <= before)).sort((a, b) => compareEventInstants(a.start, b.start) || a.event.start_at.localeCompare(b.event.start_at)).map(({ event }) => event);
|
|
4573
|
+
const offset = positiveInteger(filter.offset) || 0;
|
|
4574
|
+
const limit = positiveInteger(filter.limit);
|
|
4575
|
+
return limit ? events.slice(offset, offset + limit) : events.slice(offset);
|
|
4500
4576
|
}
|
|
4501
4577
|
function updateEvent(id, input, db) {
|
|
4502
4578
|
db = db || getDatabase();
|
|
4503
4579
|
const existing = getEvent(id, db);
|
|
4504
4580
|
if (!existing)
|
|
4505
4581
|
throw new NotFoundError("Event", id);
|
|
4582
|
+
const startAt = input.start_at ?? existing.start_at;
|
|
4583
|
+
const endAt = input.end_at ?? existing.end_at;
|
|
4584
|
+
assertEventEndsAfterStart(startAt, endAt);
|
|
4506
4585
|
db.run(`UPDATE events SET title = ?, description = ?, location = ?, start_at = ?, end_at = ?, all_day = ?, timezone = ?, status = ?, busy_type = ?, visibility = ?, recurrence_rule = ?, recurrence_exception_dates = ?, source_task_id = ?, metadata = ?, updated_at = datetime('now') WHERE id = ?`, [input.title ?? existing.title, input.description !== undefined ? input.description : existing.description, input.location !== undefined ? input.location : existing.location, input.start_at ?? existing.start_at, input.end_at ?? existing.end_at, input.all_day !== undefined ? input.all_day ? 1 : 0 : existing.all_day ? 1 : 0, input.timezone ?? existing.timezone, input.status ?? existing.status, input.busy_type ?? existing.busy_type, input.visibility ?? existing.visibility, input.recurrence_rule !== undefined ? input.recurrence_rule : existing.recurrence_rule, input.recurrence_exception_dates !== undefined ? input.recurrence_exception_dates ? JSON.stringify(input.recurrence_exception_dates) : null : existing.recurrence_exception_dates ? JSON.stringify(existing.recurrence_exception_dates) : null, input.source_task_id !== undefined ? input.source_task_id : existing.source_task_id, JSON.stringify(input.metadata ?? existing.metadata), id]);
|
|
4507
4586
|
return getEvent(id, db);
|
|
4508
4587
|
}
|
|
@@ -4514,21 +4593,22 @@ function deleteEvent(id, db) {
|
|
|
4514
4593
|
function findConflicts(calendarId, range, excludeEventId, db) {
|
|
4515
4594
|
db = db || getDatabase();
|
|
4516
4595
|
const exclude = excludeEventId ? "AND id != ?" : "";
|
|
4517
|
-
const params = excludeEventId ? [calendarId,
|
|
4518
|
-
const
|
|
4519
|
-
|
|
4596
|
+
const params = excludeEventId ? [calendarId, excludeEventId] : [calendarId];
|
|
4597
|
+
const { start: rangeStart, end: rangeEnd } = parseTimeRange(range.start, range.end);
|
|
4598
|
+
const rows = db.query(`SELECT * FROM events WHERE calendar_id = ? AND status != 'cancelled' ${exclude}`).all(...params);
|
|
4599
|
+
return rows.map(rowToEvent).map((event) => ({ event, start: parseEventTimestamp(event.start_at), end: parseEventTimestamp(event.end_at) })).filter(({ start, end }) => start < rangeEnd && end > rangeStart).sort((a, b) => compareEventInstants(a.start, b.start) || a.event.start_at.localeCompare(b.event.start_at)).map(({ event }) => event);
|
|
4520
4600
|
}
|
|
4521
4601
|
function searchEvents(query, orgId, db) {
|
|
4522
4602
|
db = db || getDatabase();
|
|
4523
4603
|
const rows = db.query(`SELECT e.* FROM events e
|
|
4524
4604
|
INNER JOIN events_fts f ON f.rowid = e.rowid
|
|
4525
4605
|
WHERE events_fts MATCH ?
|
|
4526
|
-
${orgId ? "AND e.org_id = ?" : ""}
|
|
4527
|
-
|
|
4528
|
-
return rows.map(rowToEvent);
|
|
4606
|
+
${orgId ? "AND e.org_id = ?" : ""}`).all(query, ...orgId ? [orgId] : []);
|
|
4607
|
+
return rows.map(rowToEvent).sort((a, b) => compareEventTimestampStrings(a.start_at, b.start_at));
|
|
4529
4608
|
}
|
|
4530
4609
|
var init_events = __esm(() => {
|
|
4531
4610
|
init_database();
|
|
4611
|
+
init_event_time();
|
|
4532
4612
|
init_types2();
|
|
4533
4613
|
});
|
|
4534
4614
|
|
|
@@ -4574,6 +4654,7 @@ function updateAttendee(id, input, db) {
|
|
|
4574
4654
|
}
|
|
4575
4655
|
var init_attendees = __esm(() => {
|
|
4576
4656
|
init_database();
|
|
4657
|
+
init_event_time();
|
|
4577
4658
|
init_types2();
|
|
4578
4659
|
});
|
|
4579
4660
|
|
package/dist/server/index.js
CHANGED
|
@@ -21070,6 +21070,83 @@ var init_calendars = __esm(() => {
|
|
|
21070
21070
|
init_types3();
|
|
21071
21071
|
});
|
|
21072
21072
|
|
|
21073
|
+
// src/db/event-time.ts
|
|
21074
|
+
function daysInMonth(year, month) {
|
|
21075
|
+
const date4 = new Date(0);
|
|
21076
|
+
date4.setUTCFullYear(year, month, 0);
|
|
21077
|
+
date4.setUTCHours(0, 0, 0, 0);
|
|
21078
|
+
return date4.getUTCDate();
|
|
21079
|
+
}
|
|
21080
|
+
function timestampBaseMs(year, month, day, hour, minute, second) {
|
|
21081
|
+
const date4 = new Date(0);
|
|
21082
|
+
date4.setUTCFullYear(year, month - 1, day);
|
|
21083
|
+
date4.setUTCHours(hour, minute, second, 0);
|
|
21084
|
+
return date4.getTime();
|
|
21085
|
+
}
|
|
21086
|
+
function parseEventTimestamp(value) {
|
|
21087
|
+
const match = ISO_DATE_TIME_RE.exec(value);
|
|
21088
|
+
if (!match) {
|
|
21089
|
+
throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
|
|
21090
|
+
}
|
|
21091
|
+
const year = Number(match[1]);
|
|
21092
|
+
const month = Number(match[2]);
|
|
21093
|
+
const day = Number(match[3]);
|
|
21094
|
+
const hour = Number(match[4]);
|
|
21095
|
+
const minute = Number(match[5]);
|
|
21096
|
+
const second = Number(match[6]);
|
|
21097
|
+
const fraction = match[7] || "";
|
|
21098
|
+
const offset = match[8];
|
|
21099
|
+
const maxDay = month >= 1 && month <= 12 ? daysInMonth(year, month) : 0;
|
|
21100
|
+
if (month < 1 || month > 12 || day < 1 || day > maxDay || hour > 23 || minute > 59 || second > 59) {
|
|
21101
|
+
throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
|
|
21102
|
+
}
|
|
21103
|
+
let offsetMinutes = 0;
|
|
21104
|
+
if (offset !== "Z") {
|
|
21105
|
+
const offsetSign = offset[0] === "-" ? -1 : 1;
|
|
21106
|
+
const offsetHour = Number(offset.slice(1, 3));
|
|
21107
|
+
const offsetMinute = Number(offset.slice(4, 6));
|
|
21108
|
+
if (offsetHour > 23 || offsetMinute > 59) {
|
|
21109
|
+
throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
|
|
21110
|
+
}
|
|
21111
|
+
offsetMinutes = offsetSign * (offsetHour * 60 + offsetMinute);
|
|
21112
|
+
}
|
|
21113
|
+
const timestamp = timestampBaseMs(year, month, day, hour, minute, second);
|
|
21114
|
+
if (!Number.isFinite(timestamp)) {
|
|
21115
|
+
throw new RangeError("Event start_at and end_at must be valid ISO 8601 date-time strings");
|
|
21116
|
+
}
|
|
21117
|
+
const fractionalNs = BigInt(fraction.padEnd(9, "0") || "0");
|
|
21118
|
+
return BigInt(timestamp) * NS_PER_MS + fractionalNs - BigInt(offsetMinutes) * NS_PER_MINUTE;
|
|
21119
|
+
}
|
|
21120
|
+
function assertEventEndsAfterStart(startAt, endAt) {
|
|
21121
|
+
const start = parseEventTimestamp(startAt);
|
|
21122
|
+
const end = parseEventTimestamp(endAt);
|
|
21123
|
+
if (end <= start) {
|
|
21124
|
+
throw new RangeError("Event end_at must be after start_at");
|
|
21125
|
+
}
|
|
21126
|
+
}
|
|
21127
|
+
function parseTimeRange(startAt, endAt) {
|
|
21128
|
+
const start = parseEventTimestamp(startAt);
|
|
21129
|
+
const end = parseEventTimestamp(endAt);
|
|
21130
|
+
if (end <= start) {
|
|
21131
|
+
throw new RangeError("Time range end must be after start");
|
|
21132
|
+
}
|
|
21133
|
+
return { start, end };
|
|
21134
|
+
}
|
|
21135
|
+
function compareEventInstants(a, b) {
|
|
21136
|
+
if (a < b)
|
|
21137
|
+
return -1;
|
|
21138
|
+
if (a > b)
|
|
21139
|
+
return 1;
|
|
21140
|
+
return 0;
|
|
21141
|
+
}
|
|
21142
|
+
function compareEventTimestampStrings(a, b) {
|
|
21143
|
+
return compareEventInstants(parseEventTimestamp(a), parseEventTimestamp(b)) || a.localeCompare(b);
|
|
21144
|
+
}
|
|
21145
|
+
var ISO_DATE_TIME_RE, NS_PER_MS = 1000000n, NS_PER_MINUTE = 60000000000n;
|
|
21146
|
+
var init_event_time = __esm(() => {
|
|
21147
|
+
ISO_DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?(Z|[+-]\d{2}:\d{2})$/;
|
|
21148
|
+
});
|
|
21149
|
+
|
|
21073
21150
|
// src/db/events.ts
|
|
21074
21151
|
function rowToEvent(row) {
|
|
21075
21152
|
return {
|
|
@@ -21095,9 +21172,13 @@ function rowToEvent(row) {
|
|
|
21095
21172
|
updated_at: row.updated_at
|
|
21096
21173
|
};
|
|
21097
21174
|
}
|
|
21175
|
+
function positiveInteger(value) {
|
|
21176
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
|
|
21177
|
+
}
|
|
21098
21178
|
function createEvent(input, db) {
|
|
21099
21179
|
db = db || getDatabase();
|
|
21100
21180
|
const id = crypto.randomUUID().slice(0, 8);
|
|
21181
|
+
assertEventEndsAfterStart(input.start_at, input.end_at);
|
|
21101
21182
|
db.run(`INSERT INTO events (id, calendar_id, org_id, title, description, location, start_at, end_at, all_day, timezone, status, busy_type, visibility, recurrence_rule, recurrence_exception_dates, source_task_id, created_by, metadata)
|
|
21102
21183
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.calendar_id, input.org_id, input.title, input.description || null, input.location || null, input.start_at, input.end_at, input.all_day ? 1 : 0, input.timezone || "UTC", input.status || "confirmed", input.busy_type || "busy", input.visibility || "default", input.recurrence_rule || null, input.recurrence_exception_dates ? JSON.stringify(input.recurrence_exception_dates) : null, input.source_task_id || null, input.created_by || null, JSON.stringify(input.metadata || {})]);
|
|
21103
21184
|
return getEvent(id, db);
|
|
@@ -21111,6 +21192,8 @@ function listEvents(filter = {}, db) {
|
|
|
21111
21192
|
db = db || getDatabase();
|
|
21112
21193
|
const conditions = [];
|
|
21113
21194
|
const params = [];
|
|
21195
|
+
const after = filter.after ? parseEventTimestamp(filter.after) : null;
|
|
21196
|
+
const before = filter.before ? parseEventTimestamp(filter.before) : null;
|
|
21114
21197
|
if (filter.calendar_id) {
|
|
21115
21198
|
conditions.push("calendar_id = ?");
|
|
21116
21199
|
params.push(filter.calendar_id);
|
|
@@ -21123,14 +21206,6 @@ function listEvents(filter = {}, db) {
|
|
|
21123
21206
|
conditions.push("status = ?");
|
|
21124
21207
|
params.push(filter.status);
|
|
21125
21208
|
}
|
|
21126
|
-
if (filter.after) {
|
|
21127
|
-
conditions.push("start_at >= ?");
|
|
21128
|
-
params.push(filter.after);
|
|
21129
|
-
}
|
|
21130
|
-
if (filter.before) {
|
|
21131
|
-
conditions.push("start_at <= ?");
|
|
21132
|
-
params.push(filter.before);
|
|
21133
|
-
}
|
|
21134
21209
|
if (filter.created_by) {
|
|
21135
21210
|
conditions.push("created_by = ?");
|
|
21136
21211
|
params.push(filter.created_by);
|
|
@@ -21140,16 +21215,20 @@ function listEvents(filter = {}, db) {
|
|
|
21140
21215
|
params.push(filter.source_task_id);
|
|
21141
21216
|
}
|
|
21142
21217
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
21143
|
-
const
|
|
21144
|
-
const
|
|
21145
|
-
const
|
|
21146
|
-
|
|
21218
|
+
const rows = db.query(`SELECT * FROM events ${where}`).all(...params);
|
|
21219
|
+
const events = rows.map(rowToEvent).map((event) => ({ event, start: parseEventTimestamp(event.start_at) })).filter(({ start }) => (after === null || start >= after) && (before === null || start <= before)).sort((a, b) => compareEventInstants(a.start, b.start) || a.event.start_at.localeCompare(b.event.start_at)).map(({ event }) => event);
|
|
21220
|
+
const offset = positiveInteger(filter.offset) || 0;
|
|
21221
|
+
const limit = positiveInteger(filter.limit);
|
|
21222
|
+
return limit ? events.slice(offset, offset + limit) : events.slice(offset);
|
|
21147
21223
|
}
|
|
21148
21224
|
function updateEvent(id, input, db) {
|
|
21149
21225
|
db = db || getDatabase();
|
|
21150
21226
|
const existing = getEvent(id, db);
|
|
21151
21227
|
if (!existing)
|
|
21152
21228
|
throw new NotFoundError("Event", id);
|
|
21229
|
+
const startAt = input.start_at ?? existing.start_at;
|
|
21230
|
+
const endAt = input.end_at ?? existing.end_at;
|
|
21231
|
+
assertEventEndsAfterStart(startAt, endAt);
|
|
21153
21232
|
db.run(`UPDATE events SET title = ?, description = ?, location = ?, start_at = ?, end_at = ?, all_day = ?, timezone = ?, status = ?, busy_type = ?, visibility = ?, recurrence_rule = ?, recurrence_exception_dates = ?, source_task_id = ?, metadata = ?, updated_at = datetime('now') WHERE id = ?`, [input.title ?? existing.title, input.description !== undefined ? input.description : existing.description, input.location !== undefined ? input.location : existing.location, input.start_at ?? existing.start_at, input.end_at ?? existing.end_at, input.all_day !== undefined ? input.all_day ? 1 : 0 : existing.all_day ? 1 : 0, input.timezone ?? existing.timezone, input.status ?? existing.status, input.busy_type ?? existing.busy_type, input.visibility ?? existing.visibility, input.recurrence_rule !== undefined ? input.recurrence_rule : existing.recurrence_rule, input.recurrence_exception_dates !== undefined ? input.recurrence_exception_dates ? JSON.stringify(input.recurrence_exception_dates) : null : existing.recurrence_exception_dates ? JSON.stringify(existing.recurrence_exception_dates) : null, input.source_task_id !== undefined ? input.source_task_id : existing.source_task_id, JSON.stringify(input.metadata ?? existing.metadata), id]);
|
|
21154
21233
|
return getEvent(id, db);
|
|
21155
21234
|
}
|
|
@@ -21161,21 +21240,22 @@ function deleteEvent(id, db) {
|
|
|
21161
21240
|
function findConflicts(calendarId, range, excludeEventId, db) {
|
|
21162
21241
|
db = db || getDatabase();
|
|
21163
21242
|
const exclude = excludeEventId ? "AND id != ?" : "";
|
|
21164
|
-
const params = excludeEventId ? [calendarId,
|
|
21165
|
-
const
|
|
21166
|
-
|
|
21243
|
+
const params = excludeEventId ? [calendarId, excludeEventId] : [calendarId];
|
|
21244
|
+
const { start: rangeStart, end: rangeEnd } = parseTimeRange(range.start, range.end);
|
|
21245
|
+
const rows = db.query(`SELECT * FROM events WHERE calendar_id = ? AND status != 'cancelled' ${exclude}`).all(...params);
|
|
21246
|
+
return rows.map(rowToEvent).map((event) => ({ event, start: parseEventTimestamp(event.start_at), end: parseEventTimestamp(event.end_at) })).filter(({ start, end }) => start < rangeEnd && end > rangeStart).sort((a, b) => compareEventInstants(a.start, b.start) || a.event.start_at.localeCompare(b.event.start_at)).map(({ event }) => event);
|
|
21167
21247
|
}
|
|
21168
21248
|
function searchEvents(query, orgId, db) {
|
|
21169
21249
|
db = db || getDatabase();
|
|
21170
21250
|
const rows = db.query(`SELECT e.* FROM events e
|
|
21171
21251
|
INNER JOIN events_fts f ON f.rowid = e.rowid
|
|
21172
21252
|
WHERE events_fts MATCH ?
|
|
21173
|
-
${orgId ? "AND e.org_id = ?" : ""}
|
|
21174
|
-
|
|
21175
|
-
return rows.map(rowToEvent);
|
|
21253
|
+
${orgId ? "AND e.org_id = ?" : ""}`).all(query, ...orgId ? [orgId] : []);
|
|
21254
|
+
return rows.map(rowToEvent).sort((a, b) => compareEventTimestampStrings(a.start_at, b.start_at));
|
|
21176
21255
|
}
|
|
21177
21256
|
var init_events = __esm(() => {
|
|
21178
21257
|
init_database();
|
|
21258
|
+
init_event_time();
|
|
21179
21259
|
init_types3();
|
|
21180
21260
|
});
|
|
21181
21261
|
|
|
@@ -21221,6 +21301,7 @@ function updateAttendee(id, input, db) {
|
|
|
21221
21301
|
}
|
|
21222
21302
|
var init_attendees = __esm(() => {
|
|
21223
21303
|
init_database();
|
|
21304
|
+
init_event_time();
|
|
21224
21305
|
init_types3();
|
|
21225
21306
|
});
|
|
21226
21307
|
|