This commit is contained in:
Fenris Wolf 2024-09-25 14:50:32 +02:00
parent 7ead0d85bc
commit ec775a0178
10 changed files with 641 additions and 194 deletions

View file

@ -11,14 +11,30 @@ namespace _zeitbild.api
register< register<
{ {
name : string; name : string;
public : boolean; access : {
members : Array< default_level : ("none" | "view" | "edit" | "admin");
attributed : Array<
{
user_id : int;
level : ("none" | "view" | "edit" | "admin");
}
>;
};
resource : (
{ {
user_id : user_id; kind : "local";
role : role; data : {
};
} }
>; |
resource_id : resource_id; {
kind : "caldav";
data : {
url : string;
read_only : boolean;
};
}
);
}, },
int int
>( >(
@ -33,12 +49,65 @@ namespace _zeitbild.api
}), }),
"restriction": restriction_logged_in, "restriction": restriction_logged_in,
"execution": async (stuff) => { "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 ( return (
_zeitbild.service.calendar.overview(user_id) _zeitbild.service.calendar.add(calendar_object)
.then( .then(
data => Promise.resolve({ (calendar_id : _zeitbild.type_calendar_id) => Promise.resolve({
"status_code": 200, "status_code": 200,
"data": data, "data": calendar_id,
}) })
) )
); );

View file

@ -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,
})
)
);
}
}
);
}
}

View file

@ -67,10 +67,12 @@ namespace _zeitbild.api
"type": "string", "type": "string",
"nullable": false, "nullable": false,
}, },
// TODO: fix
"begin": { "begin": {
"type": "int", "type": "int",
"nullable": false, "nullable": false,
}, },
// TODO: fix
"end": { "end": {
"type": "int", "type": "int",
"nullable": true, "nullable": true,
@ -100,7 +102,7 @@ namespace _zeitbild.api
], ],
} }
}), }),
"restriction": restriction_none, // TODO "restriction": restriction_logged_in,
"execution": async (stuff) => { "execution": async (stuff) => {
const session : {key : string; value : lib_plankton.session.type_session;} = await session_from_stuff(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); const user_id : _zeitbild.type_user_id = await _zeitbild.service.user.identify(session.value.name);

View file

@ -34,6 +34,8 @@ namespace _zeitbild.api
// calendar // calendar
{ {
_zeitbild.api.register_calendar_list(rest_subject); _zeitbild.api.register_calendar_list(rest_subject);
_zeitbild.api.register_calendar_add(rest_subject);
_zeitbild.api.register_calendar_event_add(rest_subject);
} }
// misc // misc
{ {

View file

@ -0,0 +1,182 @@
namespace _zeitbild.repository.local_resource_event
{
/**
*/
var _store : (
null
|
lib_plankton.storage.type_store<
int,
Record<string, any>,
{},
lib_plankton.storage.type_sql_table_autokey_search_term,
Record<string, any>
>
) = null;
/**
*/
function get_store(
) : lib_plankton.storage.type_store<
int,
Record<string, any>,
{},
lib_plankton.storage.type_sql_table_autokey_search_term,
Record<string, any>
>
{
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<string, any>
{
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<string, any>
) : _zeitbild.type_event_object
{
const decode_datetime : ((datetime_raw : string) => _zeitbild.helpers.type_datetime) = ((datetime_raw) => {
const parts : Array<string> = 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)
)
);
}
}

View file

@ -4,14 +4,14 @@ namespace _zeitbild.repository.resource
/** /**
*/ */
var _local_resource_core_store : ( var _local_resource_event_chest : (
null null
| |
lib_plankton.storage.type_store< lib_plankton.storage.type_chest<
int, Array<any>,
Record<string, any>, Record<string, any>,
{}, lib_plankton.database.type_description_create_table,
lib_plankton.storage.type_sql_table_autokey_search_term, lib_plankton.storage.sql_table_common.type_sql_table_common_search_term,
Record<string, any> Record<string, any>
> >
) = null; ) = null;
@ -19,7 +19,7 @@ namespace _zeitbild.repository.resource
/** /**
*/ */
var _local_resource_event_store : ( var _local_resource_core_store : (
null null
| |
lib_plankton.storage.type_store< lib_plankton.storage.type_store<
@ -62,6 +62,33 @@ namespace _zeitbild.repository.resource
) = null; ) = null;
/**
*/
function get_local_resource_event_chest(
) : lib_plankton.storage.type_chest<
Array<any>,
Record<string, any>,
lib_plankton.database.type_description_create_table,
lib_plankton.storage.sql_table_common.type_sql_table_common_search_term,
Record<string, any>
>
{
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( 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<string, any>,
{},
lib_plankton.storage.type_sql_table_autokey_search_term,
Record<string, any>
>
{
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( 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<string, any>
{
/*
const decode_datetime : ((datetime_raw : string) => _zeitbild.helpers.type_datetime) = ((datetime_raw) => {
const parts : Array<string> = 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<string, any>
) : _zeitbild.type_event_object
{
const decode_datetime : ((datetime_raw : string) => _zeitbild.helpers.type_datetime) = ((datetime_raw) => {
const parts : Array<string> = 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( export async function read(
@ -358,7 +220,7 @@ namespace _zeitbild.repository.resource
switch (dataset_core.kind) { switch (dataset_core.kind) {
case "local": { case "local": {
const dataset_extra_local_core : Record<string, any> = await get_local_resource_core_store().read(dataset_core.sub_id); const dataset_extra_local_core : Record<string, any> = await get_local_resource_core_store().read(dataset_core.sub_id);
const datasets_extra_local_events : Array<Record<string, any>> = await get_local_resource_event_store().search( const datasets_extra_local_event_ids : Array<Record<string, any>> = await get_local_resource_event_chest().search(
{ {
"expression": "(local_resource_id = $local_resource_id)", "expression": "(local_resource_id = $local_resource_id)",
"arguments": { "arguments": {
@ -370,7 +232,10 @@ namespace _zeitbild.repository.resource
{ {
"kind": "local", "kind": "local",
"data": { "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, "_dummy": null,
} }
); );
for await (const event of resource_object.data.events) { for await (const event_id of resource_object.data.event_ids) {
get_local_resource_event_store().create( await get_local_resource_event_chest().create(
encode_event( [local_resource_id, event_id]
event,
local_resource_id
)
) )
} }
const resource_id : _zeitbild.type_resource_id = await get_resource_core_store().create( const resource_id : _zeitbild.type_resource_id = await get_resource_core_store().create(
@ -445,10 +307,138 @@ namespace _zeitbild.repository.resource
break; break;
} }
default: { default: {
throw (new Error("not implemended")); throw (new Error("invalid resource kind: " + resource_object.kind));
break; break;
} }
} }
} }
/**
* @todo allow kind change?
*/
export async function update(
resource_id : _zeitbild.type_resource_id,
resource_object : _zeitbild.type_resource_object
) : Promise<void>
{
const dataset_core : Record<string, any> = 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<Record<string, any>>;} = 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<any>; preview : Record<string, any>;},
_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<void>
{
const dataset_core : Record<string, any> = 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<void>
{
const dataset_core : Record<string, any> = 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)
)
);
}
}
} }

View file

@ -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<void>
{
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( async function get_events(

View file

@ -11,4 +11,82 @@ namespace _zeitbild.service.resource
return _zeitbild.repository.resource.create(resource_object); 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<void>
{
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<void>(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<void>
{
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<void>(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));
}
}
}
} }

View file

@ -31,6 +31,11 @@ namespace _zeitbild
}; };
/**
*/
export type type_event_id = int;
/** /**
*/ */
export type type_event_object = { export type type_event_object = {
@ -65,17 +70,15 @@ namespace _zeitbild
{ {
kind : "local"; kind : "local";
data : { data : {
events : Array< event_ids : Array<type_event_id>;
type_event_object
>;
}; };
} }
| |
{ {
kind : "caldav"; kind : "caldav";
data : { data : {
read_only : boolean;
url : string; url : string;
read_only : boolean;
}; };
} }
); );

View file

@ -43,6 +43,7 @@ ${dir_temp}/zeitbild-unlinked.js: \
${dir_source}/value_objects/access_level.ts \ ${dir_source}/value_objects/access_level.ts \
${dir_source}/repositories/auth_internal.ts \ ${dir_source}/repositories/auth_internal.ts \
${dir_source}/repositories/user.ts \ ${dir_source}/repositories/user.ts \
${dir_source}/repositories/local_resource_event.ts \
${dir_source}/repositories/resource.ts \ ${dir_source}/repositories/resource.ts \
${dir_source}/repositories/calendar.ts \ ${dir_source}/repositories/calendar.ts \
${dir_source}/services/auth_internal.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_oidc.ts \
${dir_source}/api/actions/session_end.ts \ ${dir_source}/api/actions/session_end.ts \
${dir_source}/api/actions/calendar_list.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/actions/events.ts \
${dir_source}/api/functions.ts \ ${dir_source}/api/functions.ts \
${dir_source}/main.ts ${dir_source}/main.ts