From ec775a0178d13d2b9b71f82b9098894f294ae8d1 Mon Sep 17 00:00:00 2001 From: Fenris Wolf Date: Wed, 25 Sep 2024 14:50:32 +0200 Subject: [PATCH] [int] --- source/api/actions/calendar_add.ts | 87 ++++- source/api/actions/calendar_event_add.ts | 101 ++++++ source/api/actions/events.ts | 4 +- source/api/functions.ts | 2 + source/repositories/local_resource_event.ts | 182 ++++++++++ source/repositories/resource.ts | 350 ++++++++++---------- source/services/calendar.ts | 17 + source/services/resource.ts | 78 +++++ source/types.ts | 11 +- tools/makefile | 3 + 10 files changed, 641 insertions(+), 194 deletions(-) create mode 100644 source/api/actions/calendar_event_add.ts create mode 100644 source/repositories/local_resource_event.ts diff --git a/source/api/actions/calendar_add.ts b/source/api/actions/calendar_add.ts index 7718c1a..daaa4f3 100644 --- a/source/api/actions/calendar_add.ts +++ b/source/api/actions/calendar_add.ts @@ -11,14 +11,30 @@ namespace _zeitbild.api register< { name : string; - public : boolean; - members : Array< + access : { + default_level : ("none" | "view" | "edit" | "admin"); + attributed : Array< + { + user_id : int; + level : ("none" | "view" | "edit" | "admin"); + } + >; + }; + resource : ( { - user_id : user_id; - role : role; + kind : "local"; + data : { + }; } - >; - resource_id : resource_id; + | + { + kind : "caldav"; + data : { + url : string; + read_only : boolean; + }; + } + ); }, int >( @@ -33,12 +49,65 @@ namespace _zeitbild.api }), "restriction": restriction_logged_in, "execution": async (stuff) => { + const session : {key : string; value : lib_plankton.session.type_session;} = await session_from_stuff(stuff); + const user_id : _zeitbild.type_user_id = await _zeitbild.service.user.identify(session.value.name); + + // TODO move logic to calendar service + const resource_object : _zeitbild.type_resource_object = ( + { + "local": { + "kind": "local", + "data": { + "events": [], + } + }, + "caldav": { + "kind": "caldav", + "data": { + "url": stuff.input.resource.data.url, + "read_only": stuff.input.resource.data.read_only, + } + }, + }[stuff.input.resource.kind] + ); + const resource_id : _zeitbild.type_resource_id = _zeitbild.service.resource.add( + resource_object + ); + const calendar_object : _zeitbild.type_calendar_object = { + "name": stuff.input.name, + "access": { + "default_level": _zeitbild.value_object.access_level.from_string(stuff.input.access.default_level), + "attributed": lib_plankton.map.hashmap.make( + x => x.toFixed(0), + { + "pairs": ( + stuff.input.access.attributed + .map( + (entry) => ({ + "key": entry.user_id, + "value": _zeitbild.value_object.access_level.from_string(entry.level), + }) + ) + .concat( + [ + { + "key": user_id, + "value": _zeitbild.enum_access_level.admin, + } + ] + ) + ), + } + ) + }, + "resource_id": resource_id + }; return ( - _zeitbild.service.calendar.overview(user_id) + _zeitbild.service.calendar.add(calendar_object) .then( - data => Promise.resolve({ + (calendar_id : _zeitbild.type_calendar_id) => Promise.resolve({ "status_code": 200, - "data": data, + "data": calendar_id, }) ) ); diff --git a/source/api/actions/calendar_event_add.ts b/source/api/actions/calendar_event_add.ts new file mode 100644 index 0000000..234368d --- /dev/null +++ b/source/api/actions/calendar_event_add.ts @@ -0,0 +1,101 @@ + +namespace _zeitbild.api +{ + + /** + */ + export function register_calendar_event_add( + rest_subject : lib_plankton.rest.type_rest + ) : void + { + register< + { + calendar_id : int; + event : _zeitbild.type_event_object; // TODO aufdröseln + }, + null + >( + rest_subject, + lib_plankton.http.enum_method.post, + "/calendar/event_add", + { + "description": "fügt einen Termin hinzu", + "input_schema": () => ({ + "nullable": false, + "type": "object", + "properties": { + "calendar_id": { + "nullable": false, + "type": "integer", + }, + "event": { + "nullable": false, + "type": "object", + "properties": { + "name": { + "nullable": false, + "type": "string" + }, + "name": { + "nullable": false, + "type": "string" + }, + // TODO: fix + "begin": { + "type": "int", + "nullable": false, + }, + // TODO: fix + "end": { + "type": "int", + "nullable": true, + }, + "location": { + "type": "string", + "nullable": true, + }, + "description": { + "type": "string", + "nullable": true, + }, + }, + "required": [ + "name", + "begin", + "end", + "location", + "description", + ], + "additionalProperties": false + }, + }, + "required": [ + "calendar_id", + "event", + ], + "additionalProperties": false + }), + "output_schema": () => ({ + "nullable": true, + "type": "null" + }), + "restriction": restriction_logged_in, + "execution": async (stuff) => { + return ( + _zeitbild.service.calendar.event_add( + stuff.input.calendar_id, + stuff.input.event + ) + .then( + () => Promise.resolve({ + "status_code": 200, + "data": null, + }) + ) + ); + } + } + ); + } + +} diff --git a/source/api/actions/events.ts b/source/api/actions/events.ts index d602245..39e3504 100644 --- a/source/api/actions/events.ts +++ b/source/api/actions/events.ts @@ -67,10 +67,12 @@ namespace _zeitbild.api "type": "string", "nullable": false, }, + // TODO: fix "begin": { "type": "int", "nullable": false, }, + // TODO: fix "end": { "type": "int", "nullable": true, @@ -100,7 +102,7 @@ namespace _zeitbild.api ], } }), - "restriction": restriction_none, // TODO + "restriction": restriction_logged_in, "execution": async (stuff) => { const session : {key : string; value : lib_plankton.session.type_session;} = await session_from_stuff(stuff); const user_id : _zeitbild.type_user_id = await _zeitbild.service.user.identify(session.value.name); diff --git a/source/api/functions.ts b/source/api/functions.ts index 860ad60..77e47da 100644 --- a/source/api/functions.ts +++ b/source/api/functions.ts @@ -34,6 +34,8 @@ namespace _zeitbild.api // calendar { _zeitbild.api.register_calendar_list(rest_subject); + _zeitbild.api.register_calendar_add(rest_subject); + _zeitbild.api.register_calendar_event_add(rest_subject); } // misc { diff --git a/source/repositories/local_resource_event.ts b/source/repositories/local_resource_event.ts new file mode 100644 index 0000000..1ad8bde --- /dev/null +++ b/source/repositories/local_resource_event.ts @@ -0,0 +1,182 @@ + +namespace _zeitbild.repository.local_resource_event +{ + + /** + */ + var _store : ( + null + | + lib_plankton.storage.type_store< + int, + Record, + {}, + lib_plankton.storage.type_sql_table_autokey_search_term, + Record + > + ) = null; + + + /** + */ + function get_store( + ) : lib_plankton.storage.type_store< + int, + Record, + {}, + lib_plankton.storage.type_sql_table_autokey_search_term, + Record + > + { + if (_store === null) { + _store = lib_plankton.storage.sql_table_autokey_store( + { + "database_implementation": _zeitbild.database.get_implementation(), + "table_name": "events", + "key_name": "id", + } + ); + } + else { + // do nothing + } + return _store; + } + + + /** + */ + function encode( + event : _zeitbild.type_event_object, + local_resource_id : int + ) : Record + { + const encode_datetime : ((datetime : _zeitbild.helpers.type_datetime) => string) = ((datetime) => { + return lib_plankton.string.coin( + "{{timezone_shift}}|{{date}}{{macro_time}}", + { + "timezone_shift": datetime.timezone_shift.toFixed(0).padStart(2, "0"), + "date": lib_plankton.string.coin( + "{{year}}-{{month}}-{{day}}", + { + "year": datetime.date.year.toFixed(0).padStart(4, "0"), + "month": datetime.date.month.toFixed(0).padStart(2, "0"), + "day": datetime.date.day.toFixed(0).padStart(2, "0"), + } + ), + "macro_time": ( + (datetime.time === null) + ? + "" + : + lib_plankton.string.coin( + "T{{hour}}:{{minute}}:{{second}}", + { + "hour": datetime.time.hour.toFixed(0).padStart(2, "0"), + "minute": datetime.time.minute.toFixed(0).padStart(2, "0"), + "second": datetime.time.second.toFixed(0).padStart(2, "0"), + } + ) + ), + } + ); + }); + return { + "local_resource_id": local_resource_id, + "name": event.name, + "begin": encode_datetime(event.begin), + "end": ( + (event.end === null) + ? + null + : + encode_datetime(event.end) + ), + "location": event.location, + "description": event.description, + } + } + + + /** + */ + function decode( + row : Record + ) : _zeitbild.type_event_object + { + const decode_datetime : ((datetime_raw : string) => _zeitbild.helpers.type_datetime) = ((datetime_raw) => { + const parts : Array = datetime_raw.split("|"); + const timezone_shift : int = parseInt(parts[0]); + if (parts[1].length <= 10) { + return { + "timezone_shift": timezone_shift, + "date": { + "year": parseInt(parts[1].slice(0, 4)), + "month": parseInt(parts[1].slice(5, 7)), + "day": parseInt(parts[1].slice(8, 10)), + }, + "time": null + }; + } + else { + return { + "timezone_shift": timezone_shift, + "date": { + "year": parseInt(parts[1].slice(0, 4)), + "month": parseInt(parts[1].slice(5, 7)), + "day": parseInt(parts[1].slice(8, 10)), + }, + "time": { + "hour": parseInt(parts[1].slice(11, 13)), + "minute": parseInt(parts[1].slice(14, 16)), + "second": parseInt(parts[1].slice(17, 19)), + } + }; + } + }); + return { + "name": row["name"], + "begin": decode_datetime(row["begin"]), + "end": ( + (row["end"] === null) + ? + null + : + decode_datetime(row["end"]) + ), + "location": row["location"], + "description": row["description"], + }; + } + + + /** + */ + function read( + event_id : _zeitbild.type_local_resource_event_id + ) : Promise<_zeitbild.type_event_object> + { + return ( + get_store().read(event_id) + .then( + row => Promise.resolve<_zeitbild.type_event_object>(decode(row)) + ) + ); + } + + + /** + */ + function create( + event_object : _zeitbild.type_event_object + ) : Promise<_zeitbild.type_local_resource_event_id> + { + return ( + Promise.resolve(encode(event_object)) + .then( + (row) => get_store().create(row) + ) + ); + } + +} diff --git a/source/repositories/resource.ts b/source/repositories/resource.ts index 31bb35f..873021e 100644 --- a/source/repositories/resource.ts +++ b/source/repositories/resource.ts @@ -4,14 +4,14 @@ namespace _zeitbild.repository.resource /** */ - var _local_resource_core_store : ( + var _local_resource_event_chest : ( null | - lib_plankton.storage.type_store< - int, + lib_plankton.storage.type_chest< + Array, Record, - {}, - lib_plankton.storage.type_sql_table_autokey_search_term, + lib_plankton.database.type_description_create_table, + lib_plankton.storage.sql_table_common.type_sql_table_common_search_term, Record > ) = null; @@ -19,7 +19,7 @@ namespace _zeitbild.repository.resource /** */ - var _local_resource_event_store : ( + var _local_resource_core_store : ( null | lib_plankton.storage.type_store< @@ -62,6 +62,33 @@ namespace _zeitbild.repository.resource ) = null; + /** + */ + function get_local_resource_event_chest( + ) : lib_plankton.storage.type_chest< + Array, + Record, + lib_plankton.database.type_description_create_table, + lib_plankton.storage.sql_table_common.type_sql_table_common_search_term, + Record + > + { + if (_local_resource_event_chest === null) { + _local_resource_event_chest = lib_plankton.storage.sql_table_common.chest( + { + "database_implementation": _zeitbild.database.get_implementation(), + "table_name": "local_resource_events", + "key_names": ["local_resource_id","event_id"], + } + ); + } + else { + // do nothing + } + return _local_resource_event_chest; + } + + /** */ function get_local_resource_core_store( @@ -89,33 +116,6 @@ namespace _zeitbild.repository.resource } - /** - */ - function get_local_resource_event_store( - ) : lib_plankton.storage.type_store< - int, - Record, - {}, - lib_plankton.storage.type_sql_table_autokey_search_term, - Record - > - { - if (_local_resource_event_store === null) { - _local_resource_event_store = lib_plankton.storage.sql_table_autokey_store( - { - "database_implementation": _zeitbild.database.get_implementation(), - "table_name": "local_resource_events", - "key_name": "id", - } - ); - } - else { - // do nothing - } - return _local_resource_event_store; - } - - /** */ function get_caldav_resource_store( @@ -210,144 +210,6 @@ namespace _zeitbild.repository.resource */ - /** - */ - function encode_event( - event : _zeitbild.type_event_object, - local_resource_id : int - ) : Record - { - /* - const decode_datetime : ((datetime_raw : string) => _zeitbild.helpers.type_datetime) = ((datetime_raw) => { - const parts : Array = datetime_raw.split("|"); - const timezone_shift : int = parseInt(parts[0]); - if (parts[1].length <= 10) { - return { - "timezone_shift": timezone_shift, - "date": { - "year": parseInt(parts[1].slice(0, 4)), - "month": parseInt(parts[1].slice(5, 7)), - "day": parseInt(parts[1].slice(8, 10)), - }, - "time": null - }; - } - else { - return { - "timezone_shift": timezone_shift, - "date": { - "year": parseInt(parts[1].slice(0, 4)), - "month": parseInt(parts[1].slice(5, 7)), - "day": parseInt(parts[1].slice(8, 10)), - }, - "time": { - "hour": parseInt(parts[1].slice(11, 13)), - "minute": parseInt(parts[1].slice(14, 16)), - "second": parseInt(parts[1].slice(17, 19)), - } - }; - } - }); - */ - const encode_datetime : ((datetime : _zeitbild.helpers.type_datetime) => string) = ((datetime) => { - return lib_plankton.string.coin( - "{{timezone_shift}}|{{date}}{{macro_time}}", - { - "timezone_shift": datetime.timezone_shift.toFixed(0).padStart(2, "0"), - "date": lib_plankton.string.coin( - "{{year}}-{{month}}-{{day}}", - { - "year": datetime.date.year.toFixed(0).padStart(4, "0"), - "month": datetime.date.month.toFixed(0).padStart(2, "0"), - "day": datetime.date.day.toFixed(0).padStart(2, "0"), - } - ), - "macro_time": ( - (datetime.time === null) - ? - "" - : - lib_plankton.string.coin( - "T{{hour}}:{{minute}}:{{second}}", - { - "hour": datetime.time.hour.toFixed(0).padStart(2, "0"), - "minute": datetime.time.minute.toFixed(0).padStart(2, "0"), - "second": datetime.time.second.toFixed(0).padStart(2, "0"), - } - ) - ), - } - ); - }); - return { - "local_resource_id": local_resource_id, - "name": event.name, - "begin": encode_datetime(event.begin), - "end": ( - (event.end === null) - ? - null - : - encode_datetime(event.end) - ), - "location": event.location, - "description": event.description, - } - } - - - /** - */ - function decode_event( - row : Record - ) : _zeitbild.type_event_object - { - const decode_datetime : ((datetime_raw : string) => _zeitbild.helpers.type_datetime) = ((datetime_raw) => { - const parts : Array = datetime_raw.split("|"); - const timezone_shift : int = parseInt(parts[0]); - if (parts[1].length <= 10) { - return { - "timezone_shift": timezone_shift, - "date": { - "year": parseInt(parts[1].slice(0, 4)), - "month": parseInt(parts[1].slice(5, 7)), - "day": parseInt(parts[1].slice(8, 10)), - }, - "time": null - }; - } - else { - return { - "timezone_shift": timezone_shift, - "date": { - "year": parseInt(parts[1].slice(0, 4)), - "month": parseInt(parts[1].slice(5, 7)), - "day": parseInt(parts[1].slice(8, 10)), - }, - "time": { - "hour": parseInt(parts[1].slice(11, 13)), - "minute": parseInt(parts[1].slice(14, 16)), - "second": parseInt(parts[1].slice(17, 19)), - } - }; - } - }); - return { - "name": row["name"], - "begin": decode_datetime(row["begin"]), - "end": ( - (row["end"] === null) - ? - null - : - decode_datetime(row["end"]) - ), - "location": row["location"], - "description": row["description"], - }; - } - - /** */ export async function read( @@ -358,7 +220,7 @@ namespace _zeitbild.repository.resource switch (dataset_core.kind) { case "local": { const dataset_extra_local_core : Record = await get_local_resource_core_store().read(dataset_core.sub_id); - const datasets_extra_local_events : Array> = await get_local_resource_event_store().search( + const datasets_extra_local_event_ids : Array> = await get_local_resource_event_chest().search( { "expression": "(local_resource_id = $local_resource_id)", "arguments": { @@ -370,7 +232,10 @@ namespace _zeitbild.repository.resource { "kind": "local", "data": { - "events": datasets_extra_local_events.map(x => decode_event(x.preview)), + "event_ids": ( + datasets_extra_local_event_ids + .map((hit) => hit.preview["event_id"]) + ) } } ); @@ -411,12 +276,9 @@ namespace _zeitbild.repository.resource "_dummy": null, } ); - for await (const event of resource_object.data.events) { - get_local_resource_event_store().create( - encode_event( - event, - local_resource_id - ) + for await (const event_id of resource_object.data.event_ids) { + await get_local_resource_event_chest().create( + [local_resource_id, event_id] ) } const resource_id : _zeitbild.type_resource_id = await get_resource_core_store().create( @@ -445,10 +307,138 @@ namespace _zeitbild.repository.resource break; } default: { - throw (new Error("not implemended")); + throw (new Error("invalid resource kind: " + resource_object.kind)); break; } } } + + /** + * @todo allow kind change? + */ + export async function update( + resource_id : _zeitbild.type_resource_id, + resource_object : _zeitbild.type_resource_object + ) : Promise + { + const dataset_core : Record = await get_resource_core_store().read(resource_id); + if (dataset_core["kind"] !== resource_object.kind) { + return Promise.reject(new Error("resource kind may not be altered")); + } + else { + switch (resource_object.kind) { + case "local": { + // event_id list may not be altered directly + /* + const current_event_id_rows : {key : int; preview : Array>;} = await get_local_resource_event_chest().search( + { + "expression": "(local_resource_id = $local_resource_id)", + "arguments": { + "local_resource_id": dataset_core["sub_id"] + } + } + ); + const contrast : { + only_left : Array<{key : int; left : any;}>; + both : Array<{key : int; left : any; right : any;}>; + only_right : Array<{key : int; right : any;}>; + } = lib_plankton.list.contrast< + {key : Array; preview : Record;}, + _zeitbild.type_local_resource_event_id + >( + event_rows, + (hit => hit.key), + resource_object.data.event_ids, + (x => x) + ); + // TODO: single delete? + for await (const entry of constrast.only_left) { + await get_local_resource_event_store().delete( + entry.key + ); + } + /* + for await (const entry of contrast.both) { + await get_local_resource_event_store().update( + entry.left.key, + encode_event(entry.right.object) + ); + } + for await (const entry of contrast.only_right) { + const event_id : type_local_resource_event_id = await get_local_resource_event_store().create( + encode_event(entry.right.object) + ); + } + */ + break; + } + case "caldav": { + await get_caldav_resource_store().update( + dataset_core["sub_id"], + { + "url": resource_object.data.url, + "read_only": resource_object.data.read_only, + } + ); + break; + } + default: { + throw (new Error("invalid resource kind: " + resource_object.kind)); + break; + } + } + } + } + + + /** + */ + export function local_resource_event_add( + resource_id : _zeitbild.type_resource_id, + event_id : _zeitbild.type_event_id + ) : Promise + { + const dataset_core : Record = await get_resource_core_store().read(resource_id); + if (! (dataset_core.kind === "local")) { + throw (new Error("not a local resource")); + } + else { + return ( + get_local_resource_event_chest().write( + [dataset_core["sub_id"], event_id], + {} + ) + .then( + () => Promise.resolve(undefined) + ) + ); + } + } + + + /** + */ + export function local_resource_event_delete( + resource_id : _zeitbild.type_resource_id, + event_id : _zeitbild.type_event_id + ) : Promise + { + const dataset_core : Record = await get_resource_core_store().read(resource_id); + if (! (dataset_core.kind === "local")) { + throw (new Error("not a local resource")); + } + else { + return ( + get_local_resource_event_chest().delete( + [dataset_core["sub_id"], event_id], + {} + ) + .then( + () => Promise.resolve(undefined) + ) + ); + } + } + } diff --git a/source/services/calendar.ts b/source/services/calendar.ts index 5c08d0a..0eab9cf 100644 --- a/source/services/calendar.ts +++ b/source/services/calendar.ts @@ -69,6 +69,23 @@ namespace _zeitbild.service.calendar } + /** + */ + export async function event_add( + calendar_id : _zeitbild.type_calendar_id, + event_object : _zeitbild.type_event_object + ) : Promise + { + const calendar_object : _zeitbild.type_calendar_object = _zeitbild.repository.calendar.read( + calendar_id + ); + return _zeitbild.repository.resource.event_add( + calendar_object.resource_id, + event_object + ); + } + + /** */ async function get_events( diff --git a/source/services/resource.ts b/source/services/resource.ts index d4cf88c..3a3c43e 100644 --- a/source/services/resource.ts +++ b/source/services/resource.ts @@ -11,4 +11,82 @@ namespace _zeitbild.service.resource return _zeitbild.repository.resource.create(resource_object); } + + /** + */ + export async function event_add( + resource_id : _zeitbild.type_resource_id, + event_object : _zeitbild.type_event_object + ) : Promise + { + const resource_object : _zeitbild.type_resource_object = _zeitbild.repository.resource.read( + resource_id + ); + switch (resource_object.kind) { + case "local": { + const event_id : _zeitbild.type_event_id = await _zeitbild.repository.local_resource_event.create( + event_object + ); + await _zeitbild.repository.resource.local_resource_event_add( + resource_id, + event_id + ); + return Promise.resolve(undefined); + break; + } + case "caldav": { + if (resource_object.data.read_only) { + return Promise.reject(new Error("can not add event to read only caldav resource")); + } + else { + // TODO + return Promise.reject(new Error("not implemented")); + } + break; + } + default: { + throw (new Error("unhandled resource kind: " + resource_object.kind)); + } + } + } + + + /** + */ + export async function event_remove( + resource_id : _zeitbild.type_resource_id, + event_object : _zeitbild.type_event_object + ) : Promise + { + const resource_object : _zeitbild.type_resource_object = _zeitbild.repository.resource.read( + resource_id + ); + switch (resource_object.kind) { + case "local": { + const event_id : _zeitbild.type_event_id = await _zeitbild.repository.local_resource_event.create( + event_object + ); + await _zeitbild.repository.resource.local_resource_event_delete( + resource_id, + event_id + ); + return Promise.resolve(undefined); + break; + } + case "caldav": { + if (resource_object.data.read_only) { + return Promise.reject(new Error("can not delete event from read only caldav resource")); + } + else { + // TODO + return Promise.reject(new Error("not implemented")); + } + break; + } + default: { + throw (new Error("unhandled resource kind: " + resource_object.kind)); + } + } + } + } diff --git a/source/types.ts b/source/types.ts index 7e50053..6ec6e69 100644 --- a/source/types.ts +++ b/source/types.ts @@ -31,6 +31,11 @@ namespace _zeitbild }; + /** + */ + export type type_event_id = int; + + /** */ export type type_event_object = { @@ -65,17 +70,15 @@ namespace _zeitbild { kind : "local"; data : { - events : Array< - type_event_object - >; + event_ids : Array; }; } | { kind : "caldav"; data : { - read_only : boolean; url : string; + read_only : boolean; }; } ); diff --git a/tools/makefile b/tools/makefile index 8e61c4e..f1f7f27 100644 --- a/tools/makefile +++ b/tools/makefile @@ -43,6 +43,7 @@ ${dir_temp}/zeitbild-unlinked.js: \ ${dir_source}/value_objects/access_level.ts \ ${dir_source}/repositories/auth_internal.ts \ ${dir_source}/repositories/user.ts \ + ${dir_source}/repositories/local_resource_event.ts \ ${dir_source}/repositories/resource.ts \ ${dir_source}/repositories/calendar.ts \ ${dir_source}/services/auth_internal.ts \ @@ -57,6 +58,8 @@ ${dir_temp}/zeitbild-unlinked.js: \ ${dir_source}/api/actions/session_oidc.ts \ ${dir_source}/api/actions/session_end.ts \ ${dir_source}/api/actions/calendar_list.ts \ + ${dir_source}/api/actions/calendar_add.ts \ + ${dir_source}/api/actions/calendar_event_add.ts \ ${dir_source}/api/actions/events.ts \ ${dir_source}/api/functions.ts \ ${dir_source}/main.ts