nocalendar 0.2.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/README.md +65 -0
- package/index.js +282 -0
- package/nocal2-1.0.3.tgz +0 -0
- package/package.json +24 -0
- package/src/database.js +64 -0
- package/test.js +138 -0
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Nocal
|
|
2
|
+
|
|
3
|
+
A lightweight nodejs calendar library.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Create, read, update, and delete calendar events
|
|
8
|
+
- SQLite database persistence
|
|
9
|
+
- ICS (iCalendar) export and import
|
|
10
|
+
- RESTful API server
|
|
11
|
+
- Basic and Bearer authentication support
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install nocal2
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
const nocal = require('nocal2');
|
|
23
|
+
|
|
24
|
+
const cal = nocal.load('./calendar.db');
|
|
25
|
+
|
|
26
|
+
// Create an event
|
|
27
|
+
const eventId = await cal.createEvent({
|
|
28
|
+
title: 'My Event',
|
|
29
|
+
description: 'Description',
|
|
30
|
+
startDate: '2023-10-01T10:00:00Z',
|
|
31
|
+
endDate: '2023-10-01T11:00:00Z',
|
|
32
|
+
rrule: 'FREQ=WEEKLY;BYDAY=MO'
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// List events
|
|
36
|
+
const events = await cal.listEvents();
|
|
37
|
+
|
|
38
|
+
// Start API server
|
|
39
|
+
const server = await cal.start(3000);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## API
|
|
43
|
+
|
|
44
|
+
### Methods
|
|
45
|
+
|
|
46
|
+
- `load(dbPath)`: Load or create a calendar database
|
|
47
|
+
- `createEvent(eventData)`: Create a new event
|
|
48
|
+
- `listEvents()`: Get all events
|
|
49
|
+
- `updateEvent(id, updates)`: Update an event
|
|
50
|
+
- `removeEvent(id)`: Delete an event
|
|
51
|
+
- `exportIcs()`: Export events as ICS
|
|
52
|
+
- `importIcs(icsContent)`: Import events from ICS
|
|
53
|
+
- `start(port, callback)`: Start REST API server
|
|
54
|
+
|
|
55
|
+
## API Endpoints
|
|
56
|
+
|
|
57
|
+
- `GET /events`: List all events
|
|
58
|
+
- `POST /events`: Create a new event
|
|
59
|
+
- `PUT /events/:id`: Update an event
|
|
60
|
+
- `DELETE /events/:id`: Delete an event
|
|
61
|
+
- `GET /export`: Export as ICS
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
const Database = require('./src/database');
|
|
2
|
+
const { v4: uuidv4, validate: uuidValidate } = require('uuid');
|
|
3
|
+
const ICAL = require('ical.js');
|
|
4
|
+
const bcrypt = require('bcrypt');
|
|
5
|
+
|
|
6
|
+
class Nocal {
|
|
7
|
+
constructor(dbPath) {
|
|
8
|
+
this.db = new Database(dbPath);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static load(dbPath) {
|
|
12
|
+
return new Nocal(dbPath);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async createEvent(eventData) {
|
|
16
|
+
const event = {
|
|
17
|
+
id: uuidv4(),
|
|
18
|
+
title: eventData.title,
|
|
19
|
+
description: eventData.description || '',
|
|
20
|
+
startDate: eventData.startDate,
|
|
21
|
+
endDate: eventData.endDate,
|
|
22
|
+
rrule: eventData.rrule || null
|
|
23
|
+
};
|
|
24
|
+
await this.db.createEvent(event);
|
|
25
|
+
return event.id;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async listEvents() {
|
|
29
|
+
const events = await this.db.getEvents();
|
|
30
|
+
// For now, return raw events. Later expand recurring.
|
|
31
|
+
return events;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async removeEvent(eventId) {
|
|
35
|
+
const result = await this.db.deleteEvent(eventId);
|
|
36
|
+
return result.changes > 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async updateEvent(eventId, updates) {
|
|
40
|
+
const result = await this.db.updateEvent(eventId, updates);
|
|
41
|
+
return result.changes > 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async exportIcs() {
|
|
45
|
+
const events = await this.listEvents();
|
|
46
|
+
const cal = new ICAL.Component(['vcalendar', [], []]);
|
|
47
|
+
cal.updatePropertyWithValue('version', '2.0');
|
|
48
|
+
cal.updatePropertyWithValue('prodid', '-//Nocal//Calendar//EN');
|
|
49
|
+
|
|
50
|
+
events.forEach(event => {
|
|
51
|
+
const vevent = new ICAL.Component('vevent');
|
|
52
|
+
vevent.updatePropertyWithValue('uid', event.id);
|
|
53
|
+
vevent.updatePropertyWithValue('summary', event.title);
|
|
54
|
+
if (event.description) vevent.updatePropertyWithValue('description', event.description);
|
|
55
|
+
vevent.updatePropertyWithValue('dtstart', ICAL.Time.fromDateTimeString(event.start_date));
|
|
56
|
+
vevent.updatePropertyWithValue('dtend', ICAL.Time.fromDateTimeString(event.end_date));
|
|
57
|
+
if (event.rrule) {
|
|
58
|
+
// Parse RRULE
|
|
59
|
+
const rrule = ICAL.Recur.fromString(event.rrule);
|
|
60
|
+
vevent.updatePropertyWithValue('rrule', rrule);
|
|
61
|
+
}
|
|
62
|
+
cal.addSubcomponent(vevent);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return cal.toString();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async importIcs(content) {
|
|
69
|
+
const jcal = ICAL.parse(content);
|
|
70
|
+
const cal = new ICAL.Component(jcal);
|
|
71
|
+
const vevents = cal.getAllSubcomponents('vevent');
|
|
72
|
+
|
|
73
|
+
for (const vevent of vevents) {
|
|
74
|
+
const uid = vevent.getFirstPropertyValue('uid');
|
|
75
|
+
const summary = vevent.getFirstPropertyValue('summary');
|
|
76
|
+
const description = vevent.getFirstPropertyValue('description');
|
|
77
|
+
const dtstart = vevent.getFirstPropertyValue('dtstart');
|
|
78
|
+
const dtend = vevent.getFirstPropertyValue('dtend');
|
|
79
|
+
const rrule = vevent.getFirstPropertyValue('rrule');
|
|
80
|
+
|
|
81
|
+
const eventData = {
|
|
82
|
+
title: summary,
|
|
83
|
+
description: description || '',
|
|
84
|
+
startDate: dtstart.toString(),
|
|
85
|
+
endDate: dtend.toString(),
|
|
86
|
+
rrule: rrule ? rrule.toString() : null
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
await this.createEvent(eventData);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async start(port = 3000, hostOrCallback = 'localhost', optionsOrCallback = null) {
|
|
94
|
+
let host = 'localhost';
|
|
95
|
+
let options = {};
|
|
96
|
+
let callback = null;
|
|
97
|
+
|
|
98
|
+
if (typeof hostOrCallback === 'function') {
|
|
99
|
+
callback = hostOrCallback;
|
|
100
|
+
} else if (typeof hostOrCallback === 'string') {
|
|
101
|
+
host = hostOrCallback;
|
|
102
|
+
} else if (typeof hostOrCallback === 'object') {
|
|
103
|
+
options = hostOrCallback;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (typeof optionsOrCallback === 'function') {
|
|
107
|
+
callback = optionsOrCallback;
|
|
108
|
+
} else if (typeof optionsOrCallback === 'object' && optionsOrCallback !== null) {
|
|
109
|
+
options = optionsOrCallback;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Hash password for basic auth
|
|
113
|
+
if (options.auth && options.auth.type === 'basic') {
|
|
114
|
+
if (!options.auth.password.startsWith('$2')) {
|
|
115
|
+
options.auth.hashedPassword = await bcrypt.hash(options.auth.password, 10);
|
|
116
|
+
} else {
|
|
117
|
+
options.auth.hashedPassword = options.auth.password;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Auth options: { auth: { type: 'basic', username: 'user', password: 'pass' } } or { type: 'bearer', token: 'token' }
|
|
122
|
+
const http = require('http');
|
|
123
|
+
const https = require('https');
|
|
124
|
+
const MAX_PAYLOAD_SIZE = 10 * 1024; // 10kb
|
|
125
|
+
|
|
126
|
+
const serverModule = options.ssl ? https : http;
|
|
127
|
+
const server = serverModule.createServer(options.ssl || {}, async (req, res) => {
|
|
128
|
+
// Auth check
|
|
129
|
+
if (options.auth) {
|
|
130
|
+
const authHeader = req.headers.authorization;
|
|
131
|
+
let authorized = false;
|
|
132
|
+
|
|
133
|
+
if (options.auth.type === 'basic') {
|
|
134
|
+
if (authHeader && authHeader.startsWith('Basic ')) {
|
|
135
|
+
const base64Credentials = authHeader.split(' ')[1];
|
|
136
|
+
const credentials = Buffer.from(base64Credentials, 'base64').toString('ascii');
|
|
137
|
+
const [username, password] = credentials.split(':');
|
|
138
|
+
if (username === options.auth.username && await bcrypt.compare(password, options.auth.hashedPassword)) {
|
|
139
|
+
authorized = true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} else if (options.auth.type === 'bearer') {
|
|
143
|
+
if (authHeader && authHeader.startsWith('Bearer ') && authHeader.split(' ')[1] === options.auth.token) {
|
|
144
|
+
authorized = true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!authorized) {
|
|
149
|
+
res.writeHead(401, { 'WWW-Authenticate': options.auth.type === 'basic' ? 'Basic realm="Nocal API"' : 'Bearer' });
|
|
150
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const parsedUrl = new URL(req.url, 'http://dummy');
|
|
156
|
+
const method = req.method;
|
|
157
|
+
const path = parsedUrl.pathname;
|
|
158
|
+
|
|
159
|
+
res.setHeader('Content-Type', 'application/json');
|
|
160
|
+
|
|
161
|
+
// Validate event ID for routes that use it
|
|
162
|
+
let eventId = null;
|
|
163
|
+
if (path.startsWith('/events/')) {
|
|
164
|
+
const parts = path.split('/');
|
|
165
|
+
if (parts.length === 3 && parts[1] === 'events') {
|
|
166
|
+
eventId = parts[2];
|
|
167
|
+
if (!eventId || !uuidValidate(eventId)) {
|
|
168
|
+
res.writeHead(400);
|
|
169
|
+
res.end(JSON.stringify({ error: 'Invalid event ID' }));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (path === '/events' && method === 'GET') {
|
|
176
|
+
try {
|
|
177
|
+
const events = await this.listEvents();
|
|
178
|
+
res.writeHead(200);
|
|
179
|
+
res.end(JSON.stringify(events));
|
|
180
|
+
} catch (err) {
|
|
181
|
+
res.writeHead(500);
|
|
182
|
+
res.end(JSON.stringify({ error: 'Internal server error' }));
|
|
183
|
+
}
|
|
184
|
+
} else if (path === '/events' && method === 'POST') {
|
|
185
|
+
let body = '';
|
|
186
|
+
let bodySize = 0;
|
|
187
|
+
req.on('data', chunk => {
|
|
188
|
+
bodySize += chunk.length;
|
|
189
|
+
if (bodySize > MAX_PAYLOAD_SIZE) {
|
|
190
|
+
res.writeHead(413);
|
|
191
|
+
res.end(JSON.stringify({ error: 'Payload too large' }));
|
|
192
|
+
req.destroy();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
body += chunk;
|
|
196
|
+
});
|
|
197
|
+
req.on('end', async () => {
|
|
198
|
+
try {
|
|
199
|
+
const eventData = JSON.parse(body);
|
|
200
|
+
if (!eventData.title || !eventData.startDate || !eventData.endDate) {
|
|
201
|
+
res.writeHead(400);
|
|
202
|
+
res.end(JSON.stringify({ error: 'Missing required fields: title, startDate, endDate' }));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const id = await this.createEvent(eventData);
|
|
206
|
+
res.writeHead(201);
|
|
207
|
+
res.end(JSON.stringify({ id }));
|
|
208
|
+
} catch (err) {
|
|
209
|
+
res.writeHead(400);
|
|
210
|
+
res.end(JSON.stringify({ error: 'Invalid JSON or data' }));
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
} else if (path.startsWith('/events/') && method === 'PUT') {
|
|
214
|
+
let body = '';
|
|
215
|
+
let bodySize = 0;
|
|
216
|
+
req.on('data', chunk => {
|
|
217
|
+
bodySize += chunk.length;
|
|
218
|
+
if (bodySize > MAX_PAYLOAD_SIZE) {
|
|
219
|
+
res.writeHead(413);
|
|
220
|
+
res.end(JSON.stringify({ error: 'Payload too large' }));
|
|
221
|
+
req.destroy();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
body += chunk;
|
|
225
|
+
});
|
|
226
|
+
req.on('end', async () => {
|
|
227
|
+
try {
|
|
228
|
+
const updates = JSON.parse(body);
|
|
229
|
+
delete updates.id; // Prevent updating id
|
|
230
|
+
const success = await this.updateEvent(eventId, updates);
|
|
231
|
+
if (success) {
|
|
232
|
+
res.writeHead(200);
|
|
233
|
+
res.end(JSON.stringify({ message: 'Updated' }));
|
|
234
|
+
} else {
|
|
235
|
+
res.writeHead(404);
|
|
236
|
+
res.end(JSON.stringify({ error: 'Event not found' }));
|
|
237
|
+
}
|
|
238
|
+
} catch (err) {
|
|
239
|
+
res.writeHead(400);
|
|
240
|
+
res.end(JSON.stringify({ error: 'Invalid JSON or data' }));
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
} else if (path.startsWith('/events/') && method === 'DELETE') {
|
|
244
|
+
try {
|
|
245
|
+
const success = await this.removeEvent(eventId);
|
|
246
|
+
if (success) {
|
|
247
|
+
res.writeHead(200);
|
|
248
|
+
res.end(JSON.stringify({ message: 'Deleted' }));
|
|
249
|
+
} else {
|
|
250
|
+
res.writeHead(404);
|
|
251
|
+
res.end(JSON.stringify({ error: 'Event not found' }));
|
|
252
|
+
}
|
|
253
|
+
} catch (err) {
|
|
254
|
+
res.writeHead(500);
|
|
255
|
+
res.end(JSON.stringify({ error: 'Internal server error' }));
|
|
256
|
+
}
|
|
257
|
+
} else if (path === '/export' && method === 'GET') {
|
|
258
|
+
try {
|
|
259
|
+
const ics = await this.exportIcs();
|
|
260
|
+
res.setHeader('Content-Type', 'text/calendar');
|
|
261
|
+
res.writeHead(200);
|
|
262
|
+
res.end(ics);
|
|
263
|
+
} catch (err) {
|
|
264
|
+
res.writeHead(500);
|
|
265
|
+
res.end(JSON.stringify({ error: 'Internal server error' }));
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
res.writeHead(404);
|
|
269
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
server.listen(port, host, () => {
|
|
274
|
+
console.log(`Nocal API listening on http://${host}:${port}`);
|
|
275
|
+
if (callback) callback();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
return server;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
module.exports = { load: Nocal.load };
|
package/nocal2-1.0.3.tgz
ADDED
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nocalendar",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A lightweight nodejs calendar library. ",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node test.js"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"calendar",
|
|
11
|
+
"ics",
|
|
12
|
+
"sqlite",
|
|
13
|
+
"nodejs",
|
|
14
|
+
"nocal",
|
|
15
|
+
"nocal2"
|
|
16
|
+
],
|
|
17
|
+
"author": "little codex",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"ical.js": "^1.5.0",
|
|
21
|
+
"uuid": "^9.0.1",
|
|
22
|
+
"bcrypt": "^5.1.1"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/database.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
2
|
+
|
|
3
|
+
class Database {
|
|
4
|
+
constructor(dbPath) {
|
|
5
|
+
this.db = new DatabaseSync(dbPath);
|
|
6
|
+
this.init();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
init() {
|
|
10
|
+
const sql = `
|
|
11
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
12
|
+
id TEXT PRIMARY KEY,
|
|
13
|
+
title TEXT NOT NULL,
|
|
14
|
+
description TEXT,
|
|
15
|
+
start_date TEXT NOT NULL,
|
|
16
|
+
end_date TEXT NOT NULL,
|
|
17
|
+
rrule TEXT,
|
|
18
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
19
|
+
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
20
|
+
);
|
|
21
|
+
`;
|
|
22
|
+
this.db.exec(sql);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Methods for CRUD
|
|
26
|
+
createEvent(event) {
|
|
27
|
+
const sql = `
|
|
28
|
+
INSERT INTO events (id, title, description, start_date, end_date, rrule)
|
|
29
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
30
|
+
`;
|
|
31
|
+
const stmt = this.db.prepare(sql);
|
|
32
|
+
stmt.run(event.id, event.title, event.description, event.startDate, event.endDate, event.rrule);
|
|
33
|
+
return { id: event.id };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getEvents() {
|
|
37
|
+
const sql = 'SELECT * FROM events';
|
|
38
|
+
const stmt = this.db.prepare(sql);
|
|
39
|
+
return stmt.all();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
updateEvent(id, updates) {
|
|
43
|
+
const fields = Object.keys(updates).map(key => `${key} = ?`).join(', ');
|
|
44
|
+
const values = Object.values(updates);
|
|
45
|
+
values.push(id);
|
|
46
|
+
const sql = `UPDATE events SET ${fields}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`;
|
|
47
|
+
const stmt = this.db.prepare(sql);
|
|
48
|
+
const result = stmt.run(...values);
|
|
49
|
+
return { changes: result.changes };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
deleteEvent(id) {
|
|
53
|
+
const sql = 'DELETE FROM events WHERE id = ?';
|
|
54
|
+
const stmt = this.db.prepare(sql);
|
|
55
|
+
const result = stmt.run(id);
|
|
56
|
+
return { changes: result.changes };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
close() {
|
|
60
|
+
this.db.close();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = Database;
|
package/test.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const nocal = require('./index');
|
|
2
|
+
|
|
3
|
+
async function test() {
|
|
4
|
+
const cal = nocal.load('./test.db');
|
|
5
|
+
|
|
6
|
+
console.log('Testing createEvent...');
|
|
7
|
+
const id = await cal.createEvent({
|
|
8
|
+
title: 'My Event',
|
|
9
|
+
description: 'My personal event',
|
|
10
|
+
startDate: '2023-10-01T10:00:00Z',
|
|
11
|
+
endDate: '2023-10-01T11:00:00Z',
|
|
12
|
+
rrule: 'FREQ=WEEKLY;BYDAY=MO'
|
|
13
|
+
});
|
|
14
|
+
console.log('Created event:', id);
|
|
15
|
+
|
|
16
|
+
console.log('Testing listEvents...');
|
|
17
|
+
let events = await cal.listEvents();
|
|
18
|
+
console.log('Events count:', events.length);
|
|
19
|
+
|
|
20
|
+
console.log('Testing updateEvent...');
|
|
21
|
+
const updated = await cal.updateEvent(id, { title: 'Updated Event', description: 'Updated description' });
|
|
22
|
+
console.log('Update result:', updated);
|
|
23
|
+
|
|
24
|
+
console.log('Testing listEvents after update...');
|
|
25
|
+
events = await cal.listEvents();
|
|
26
|
+
console.log('First event title:', events[0].title);
|
|
27
|
+
|
|
28
|
+
console.log('Testing exportIcs...');
|
|
29
|
+
const ics = await cal.exportIcs();
|
|
30
|
+
console.log('ICS exported, length:', ics.length);
|
|
31
|
+
|
|
32
|
+
console.log('Testing importIcs...');
|
|
33
|
+
// Create a simple ICS content for import
|
|
34
|
+
const icsImport = `BEGIN:VCALENDAR
|
|
35
|
+
VERSION:2.0
|
|
36
|
+
BEGIN:VEVENT
|
|
37
|
+
UID:test-import-123
|
|
38
|
+
SUMMARY:Imported Event
|
|
39
|
+
DESCRIPTION:Test import
|
|
40
|
+
DTSTART:20231002T120000Z
|
|
41
|
+
DTEND:20231002T130000Z
|
|
42
|
+
END:VEVENT
|
|
43
|
+
END:VCALENDAR`;
|
|
44
|
+
await cal.importIcs(icsImport);
|
|
45
|
+
console.log('ICS imported');
|
|
46
|
+
|
|
47
|
+
console.log('Testing listEvents after import...');
|
|
48
|
+
events = await cal.listEvents();
|
|
49
|
+
console.log('Events count after import:', events.length);
|
|
50
|
+
|
|
51
|
+
console.log('Testing removeEvent...');
|
|
52
|
+
const removed = await cal.removeEvent(id);
|
|
53
|
+
console.log('Remove result:', removed);
|
|
54
|
+
|
|
55
|
+
console.log('Testing listEvents after remove...');
|
|
56
|
+
events = await cal.listEvents();
|
|
57
|
+
console.log('Events count after remove:', events.length);
|
|
58
|
+
|
|
59
|
+
console.log('Testing start API...');
|
|
60
|
+
const server = await cal.start(3000, () => {
|
|
61
|
+
console.log('API started for testing');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Wait a bit for server to start
|
|
65
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
66
|
+
|
|
67
|
+
// Test API endpoints
|
|
68
|
+
const http = require('http');
|
|
69
|
+
|
|
70
|
+
// Helper function to make request
|
|
71
|
+
function makeRequest(options, data = null) {
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
const req = http.request(options, (res) => {
|
|
74
|
+
let body = '';
|
|
75
|
+
res.on('data', chunk => body += chunk);
|
|
76
|
+
res.on('end', () => {
|
|
77
|
+
try {
|
|
78
|
+
const json = JSON.parse(body);
|
|
79
|
+
resolve({ status: res.statusCode, data: json });
|
|
80
|
+
} catch (e) {
|
|
81
|
+
resolve({ status: res.statusCode, data: body });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
req.on('error', reject);
|
|
86
|
+
if (data) {
|
|
87
|
+
req.write(JSON.stringify(data));
|
|
88
|
+
}
|
|
89
|
+
req.end();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Test GET /events
|
|
94
|
+
console.log('Testing GET /events...');
|
|
95
|
+
let response = await makeRequest({ hostname: 'localhost', port: 3000, path: '/events', method: 'GET' });
|
|
96
|
+
console.log('GET /events status:', response.status, 'events:', response.data.length);
|
|
97
|
+
|
|
98
|
+
// Test POST /events
|
|
99
|
+
console.log('Testing POST /events...');
|
|
100
|
+
response = await makeRequest({
|
|
101
|
+
hostname: 'localhost', port: 3000, path: '/events', method: 'POST',
|
|
102
|
+
headers: { 'Content-Type': 'application/json' }
|
|
103
|
+
}, {
|
|
104
|
+
title: 'API Test Event',
|
|
105
|
+
description: 'Created via API',
|
|
106
|
+
startDate: '2023-10-03T14:00:00Z',
|
|
107
|
+
endDate: '2023-10-03T15:00:00Z'
|
|
108
|
+
});
|
|
109
|
+
console.log('POST /events status:', response.status, 'id:', response.data.id);
|
|
110
|
+
const apiEventId = response.data.id;
|
|
111
|
+
|
|
112
|
+
// Test PUT /events/:id
|
|
113
|
+
console.log('Testing PUT /events/:id...');
|
|
114
|
+
response = await makeRequest({
|
|
115
|
+
hostname: 'localhost', port: 3000, path: `/events/${apiEventId}`, method: 'PUT',
|
|
116
|
+
headers: { 'Content-Type': 'application/json' }
|
|
117
|
+
}, { title: 'Updated via API' });
|
|
118
|
+
console.log('PUT /events/:id status:', response.status);
|
|
119
|
+
|
|
120
|
+
// Test GET /export
|
|
121
|
+
console.log('Testing GET /export...');
|
|
122
|
+
response = await makeRequest({ hostname: 'localhost', port: 3000, path: '/export', method: 'GET' });
|
|
123
|
+
console.log('GET /export status:', response.status, 'ICS length:', response.data.length);
|
|
124
|
+
|
|
125
|
+
// Test DELETE /events/:id
|
|
126
|
+
console.log('Testing DELETE /events/:id...');
|
|
127
|
+
response = await makeRequest({ hostname: 'localhost', port: 3000, path: `/events/${apiEventId}`, method: 'DELETE' });
|
|
128
|
+
console.log('DELETE /events/:id status:', response.status);
|
|
129
|
+
|
|
130
|
+
// Close server
|
|
131
|
+
server.close(() => {
|
|
132
|
+
console.log('API server closed');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
console.log('All tests completed successfully!');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
test().catch(console.error);
|