This commit is contained in:
Fenris Wolf 2024-09-12 16:35:57 +02:00
parent 175e57b1f3
commit 85f16e3c3b
7 changed files with 711 additions and 360 deletions

View file

@ -1,21 +1,23 @@
- Kalender sollen unabhängig von Nutzern bestehen können - Kalender sollen unabhängig von Nutzern bestehen können
- einem Kalender können beliebig viele Nutzer zugeordnet werden, die jeweils bestimmte Berechtigungen haben (z.B. als Rollen "admin", "editor", "viewer", …) - einem Kalender können beliebig viele Nutzer zugeordnet werden, die jeweils bestimmte Berechtigungen haben (z.B. als Rollen "admin", "editor", "viewer", …)
- Events bilden keine Domäne - Veranstaltungen bilden keine Domäne
- es gibt verschiedene Arten von Quellen:
- lokal
- enthält Veranstaltungen
- caldav
- enthält keine eigenen Veranstaltungen
- sollte read-only- und read/write-Modus haben
- Berechtigungen: - Berechtigungen:
- Kalender anlegen - Kalender anlegen
- Kalender-Stammdaten ändern - Stammdaten ändern
- Kalender-Einträge lesen - Einträge lesen
- Kalender-Einträge erstellen - Einträge erstellen
- Kalender-Einträge ändern - Einträge ändern
- Kalender-Einträge entfernen - Einträge entfernen
- Rollen (innerhalb eines Kalendars):
- `admin`: kann alles
- `editor`: kann bei lokalen
- Kalender sind für gewöhnlichen öffentlich - Kalender sind für gewöhnlichen öffentlich
- es gibt verschiedene Arten von Kalendern:
- konkret
- enthält Veranstaltungen
- extern
- über CalDAV
- sollte read-only- und read/write-Modus haben
- nach dem Anmelden sieht man eine Kalender-Ansicht mit folgenden Kalendern kombiniert angezeigt: - nach dem Anmelden sieht man eine Kalender-Ansicht mit folgenden Kalendern kombiniert angezeigt:
- öffentliche Kalender - öffentliche Kalender
- nicht öffentliche Kalendar, bei welchen man Lese-Berechtigung hat - nicht öffentliche Kalendar, bei welchen man Lese-Berechtigung hat
- Entwurfsname: "zeitbild"

View file

@ -4,69 +4,6 @@
namespace _zeitbild.helpers namespace _zeitbild.helpers
{ {
/**
*/
var _template_cache : Record<string, string> = {};
/**
* @todo caching
*/
export async function template_coin(
name : string,
data : Record<string, string>
) : Promise<string>
{
let content : string;
if (! (name in _template_cache)) {
content = (
(
await lib_plankton.file.read(
lib_plankton.string.coin(
"templates/{{name}}.html.tpl",
{
"name": name,
}
)
)
)
.toString()
);
_template_cache[name] = content;
}
else {
content = _template_cache[name];
}
return Promise.resolve<string>(
lib_plankton.string.coin(
content,
data
)
);
}
/**
* @todo outsource
*/
export async function promise_row<type_result>(
members : Array<
() => Promise<type_result>
>
) : Promise<
Array<
type_result
>
>
{
let results : Array<type_result> = [];
for await (const member of members) {
results.push(await member());
}
return Promise.resolve<Array<type_result>>(results);
}
/** /**
*/ */
export function date_object_get_week_of_year( export function date_object_get_week_of_year(
@ -153,7 +90,6 @@ namespace _zeitbild.helpers
} }
/** /**
* @todo negative shift? * @todo negative shift?
*/ */
@ -587,4 +523,67 @@ namespace _zeitbild.helpers
}; };
} }
/**
*/
var _template_cache : Record<string, string> = {};
/**
* @todo caching
*/
export async function template_coin(
name : string,
data : Record<string, string>
) : Promise<string>
{
let content : string;
if (! (name in _template_cache)) {
content = (
(
await lib_plankton.file.read(
lib_plankton.string.coin(
"templates/{{name}}.html.tpl",
{
"name": name,
}
)
)
)
.toString()
);
_template_cache[name] = content;
}
else {
content = _template_cache[name];
}
return Promise.resolve<string>(
lib_plankton.string.coin(
content,
data
)
);
}
/**
* @todo outsource
*/
export async function promise_row<type_result>(
members : Array<
() => Promise<type_result>
>
) : Promise<
Array<
type_result
>
>
{
let results : Array<type_result> = [];
for await (const member of members) {
results.push(await member());
}
return Promise.resolve<Array<type_result>>(results);
}
} }

View file

@ -2,12 +2,28 @@
namespace _zeitbild.repository.calendar namespace _zeitbild.repository.calendar
{ {
/**
*/
type type_dispersal = {
core_row : Record<
string,
any
>;
member_rows : Array<
Record<
string,
any
>
>;
};
/** /**
*/ */
var _core_store : ( var _core_store : (
null null
| |
lib_plankton.storage.type_core_store< lib_plankton.storage.type_store<
_zeitbild.type.calendar_id, _zeitbild.type.calendar_id,
Record<string, any>, Record<string, any>,
{}, {},
@ -19,14 +35,14 @@ namespace _zeitbild.repository.calendar
/** /**
*/ */
var _event_store : ( var _member_chest : (
null null
| |
lib_plankton.storage.type_core_store< lib_plankton.storage.type_chest<
_zeitbild.type.event_id, 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;
@ -35,7 +51,7 @@ namespace _zeitbild.repository.calendar
/** /**
*/ */
function get_core_store( function get_core_store(
) : lib_plankton.storage.type_core_store< ) : lib_plankton.storage.type_store<
_zeitbild.type.calendar_id, _zeitbild.type.calendar_id,
Record<string, any>, Record<string, any>,
{}, {},
@ -44,7 +60,7 @@ namespace _zeitbild.repository.calendar
> >
{ {
if (_core_store === null) { if (_core_store === null) {
_core_store = lib_plankton.storage.sql_table_autokey_core_store( _core_store = lib_plankton.storage.sql_table_autokey_store(
{ {
"database_implementation": _zeitbild.database.get_implementation(), "database_implementation": _zeitbild.database.get_implementation(),
"table_name": "calendars", "table_name": "calendars",
@ -61,102 +77,78 @@ namespace _zeitbild.repository.calendar
/** /**
*/ */
function get_event_store( function get_member_chest(
) : lib_plankton.storage.type_core_store< ) : lib_plankton.storage.type_chest<
_zeitbild.type.event_id, 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>
> >
{ {
if (_event_store === null) { if (_member_chest === null) {
_event_store = lib_plankton.storage.sql_table_autokey_core_store( _member_chest = lib_plankton.storage.sql_table_common.chest(
{ {
"database_implementation": _zeitbild.database.get_implementation(), "database_implementation": _zeitbild.database.get_implementation(),
"table_name": "events", "table_name": "calendar_members",
"key_name": "id", "key_names": ["calendar_id","user_id"],
} }
); );
} }
else { else {
// do nothing // do nothing
} }
return _event_store; return _member_chest;
} }
/** /**
* @todo use events table
*/ */
function encode( function encode(
object : _zeitbild.type.calendar_object object : _zeitbild.type.calendar_object
) : Record<string, any> ) : type_dispersal
{ {
switch (object.kind) {
/*
case "concrete": {
const data_raw : any = lib_plankton.json.encode(object.data);
data_raw["users"]
return { return {
"core_row": {
"name": object.name, "name": object.name,
"private": object.private, "public": object.public,
"kind": object.kind, "resource_id": object.resource_id,
"data": {
"users": data_raw["users"],
"events": [] // TODO
}, },
"member_rows": (
object.members
.map(
(member) => ({
// "calendar_id": calendar_id,
"user_id": member.user_id,
"role": member.role,
})
)
),
}; };
} }
*/
default: {
return {
"name": object.name,
"private": object.private,
"kind": object.kind,
"data": lib_plankton.json.encode(object.data),
};
}
}
}
/** /**
*/ */
function decode( function decode(
row : Record<string, any> dispersal : type_dispersal
) : _zeitbild.type.calendar_object ) : _zeitbild.type.calendar_object
{ {
return { return {
"name": row["name"], "name": dispersal.core_row["name"],
"private": row["private"], "public": dispersal.core_row["public"],
"kind": row["kind"], "members": (
"data": lib_plankton.json.decode(row["data"]), dispersal.member_rows
};
}
/**
*/
export async function dump(
) : Promise<
Array<
{
id : _zeitbild.type.calendar_id;
object : _zeitbild.type.calendar_object;
}
>
>
{
return (
(await get_core_store().search(null))
.map( .map(
({"key": key, "preview": preview}) => ({ (member_row) => ({
"id": key, "calendar_id": member_row["calendar_id"],
"object": (preview as _zeitbild.type.calendar_object), "user_id": member_row["user_id"],
"role": member_row["role"],
}) })
) )
); ),
"resource_id": dispersal.core_row["resource_id"],
};
} }
@ -177,7 +169,15 @@ namespace _zeitbild.repository.calendar
> >
{ {
return ( return (
(await get_core_store().search(null)) (
await get_core_store().search(
{
"expression": "(public = TRUE)",
"arguments": {
}
}
)
)
.filter( .filter(
({"key": key, "preview": preview}) => ( ({"key": key, "preview": preview}) => (
( (
@ -205,49 +205,74 @@ namespace _zeitbild.repository.calendar
/** /**
*/ */
export async function read( export function read(
id : _zeitbild.type.calendar_id id : _zeitbild.type.calendar_id
) : Promise<_zeitbild.type.calendar_object> ) : Promise<_zeitbild.type.calendar_object>
{ {
const row : Record<string, any> = await get_core_store().read(id); return (
get_core_store().read(id)
return decode(row); .then(
(core_row) => (
get_member_chest().search(
{
"expression": "(calendar_id = $calendar_id)",
"arguments": {
"calendar_id": id,
}
}
)
.then(
(member_rows) => Promise.resolve<type_dispersal>(
{
"core_row": core_row,
"member_rows": member_rows,
}
)
)
.then(
(dispersal) => Promise.resolve<_zeitbild.type.calendar_object>(
decode(dispersal)
)
)
)
)
);
} }
/** /**
*/ */
export async function create( export function create(
value : _zeitbild.type.calendar_object calendar_object : _zeitbild.type.calendar_object
) : Promise<_zeitbild.type.calendar_id> ) : Promise<_zeitbild.type.calendar_id>
{ {
const row : Record<string, any> = encode(value); return (
const id : _zeitbild.type.calendar_id = await get_core_store().create(row); Promise.resolve<_zeitbild.type.calendar_object>(calendar_object)
.then<type_dispersal>(
return id; (calendar_object) => Promise.resolve<type_dispersal>(encode(calendar_object))
} )
.then<_zeitbild.type.calendar_id>(
(dispersal) => (
/** get_core_store().create(dispersal.core_row)
*/ .then<_zeitbild.type.calendar_id>(
export async function update( (calendar_id) => (
id : _zeitbild.type.calendar_id, Promise.all(
value : _zeitbild.type.calendar_object dispersal.member_rows
) : Promise<void> .map(
{ (member_row) => get_member_chest().write(
const row : Record<string, any> = encode(value); [calendar_id, member_row["user_id"]],
{"role": member_row["role"]}
await get_core_store().update(id, row); )
} )
)
.then(
/** () => Promise.resolve<_zeitbild.type.calendar_id>(calendar_id)
*/ )
export async function delete_( )
id : _zeitbild.type.calendar_id )
) : Promise<void> )
{ )
await get_core_store().delete(id); );
} }
} }

View file

@ -0,0 +1,314 @@
namespace _zeitbild.repository.resource
{
/**
*/
var _local_resource_core_store : (
null
|
lib_plankton.storage.type_store<
int,
Record<string, any>,
{},
lib_plankton.storage.type_sql_table_autokey_search_term,
Record<string, any>
>
) = null;
/**
*/
var _local_resource_event_store : (
null
|
lib_plankton.storage.type_store<
int,
Record<string, any>,
{},
lib_plankton.storage.type_sql_table_autokey_search_term,
Record<string, any>
>
) = null;
/**
*/
var _caldav_resource_store : (
null
|
lib_plankton.storage.type_store<
int,
Record<string, any>,
{},
lib_plankton.storage.type_sql_table_autokey_search_term,
Record<string, any>
>
) = null;
/**
*/
var _resource_core_store : (
null
|
lib_plankton.storage.type_store<
_zeitbild.type.resource_id,
Record<string, any>,
{},
lib_plankton.storage.type_sql_table_autokey_search_term,
Record<string, any>
>
) = null;
/**
*/
function get_local_resource_core_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_core_store === null) {
_local_resource_core_store = lib_plankton.storage.sql_table_autokey_store(
{
"database_implementation": _zeitbild.database.get_implementation(),
"table_name": "local_resources",
"key_name": "id",
}
);
}
else {
// do nothing
}
return _local_resource_core_store;
}
/**
*/
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(
) : lib_plankton.storage.type_store<
int,
Record<string, any>,
{},
lib_plankton.storage.type_sql_table_autokey_search_term,
Record<string, any>
>
{
if (_caldav_resource_store === null) {
_caldav_resource_store = lib_plankton.storage.sql_table_autokey_store(
{
"database_implementation": _zeitbild.database.get_implementation(),
"table_name": "caldav_resources",
"key_name": "id",
}
);
}
else {
// do nothing
}
return _caldav_resource_store;
}
/**
*/
function get_resource_core_store(
) : lib_plankton.storage.type_store<
_zeitbild.type.resource_id,
Record<string, any>,
{},
lib_plankton.storage.type_sql_table_autokey_search_term,
Record<string, any>
>
{
if (_resource_core_store === null) {
_resource_core_store = lib_plankton.storage.sql_table_autokey_store(
{
"database_implementation": _zeitbild.database.get_implementation(),
"table_name": "resources",
"key_name": "id",
}
);
}
else {
// do nothing
}
return _resource_core_store;
}
/**
*/
/*
function encode_resource_core(
stuff : {
object : _zeitbild.type.resource_object;
sub_id : int;
}
) : Record<string, any>
{
return {
"kind": stuff.object.kind,
"sub_id": stuff.sub_id
};
}
*/
/**
* @todo data
*/
/*
function decode_resource_core(
row : Record<string, any>
) : {
object : _zeitbild.type.resource_object;
sub_id : int;
}
{
return {
"object": {
"kind": row["kind"],
"data": null,
},
"sub_id": row["sub_id"],
};
}
*/
/**
*/
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(
resource_id : _zeitbild.type.resource_id
) : Promise<_zeitbild.type.resource_object>
{
const dataset_core : Record<string, any> = await get_resource_core_store().read(resource_id);
switch (dataset_core.kind) {
case "local": {
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(
{
"expression": "(local_resource_id = $local_resource_id)",
"arguments": {
"local_resource_id": dataset_core.sub_id,
}
}
);
return Promise.resolve<_zeitbild.type.resource_object>(
{
"kind": "local",
"data": {
"events": datasets_extra_local_events.map(x => decode_event(x)),
}
}
);
}
case "caldav": {
const dataset_extra_caldav : Record<string, any> = await get_caldav_resource_store().read(dataset_core.sub_id);
return Promise.resolve<_zeitbild.type.resource_object>(
{
"kind": "caldav",
"data": {
"url": dataset_extra_caldav["url"],
"read_only": dataset_extra_caldav["read_only"],
}
}
);
break;
}
default: {
return Promise.reject<_zeitbild.type.resource_object>(
new Error("invalid resource kind: " + dataset_core.kind)
);
break;
}
}
}
}

View file

@ -42,9 +42,8 @@ namespace _zeitbild.service.calendar
/** /**
* @todo prevent loops
*/ */
export async function gather_events( async function gather_events(
calendar_ids : Array<_zeitbild.type.calendar_id>, calendar_ids : Array<_zeitbild.type.calendar_id>,
from_pit : _zeitbild.helpers.type_pit, from_pit : _zeitbild.helpers.type_pit,
to_pit : _zeitbild.helpers.type_pit to_pit : _zeitbild.helpers.type_pit
@ -52,44 +51,26 @@ namespace _zeitbild.service.calendar
Array< Array<
{ {
calendar_id : _zeitbild.type.calendar_id; calendar_id : _zeitbild.type.calendar_id;
calendar_name : string;
event : _zeitbild.type.event_object; event : _zeitbild.type.event_object;
} }
> >
> >
{ {
lib_plankton.log.info(
"calendar_gather_events",
{
"calendar_ids": calendar_ids,
}
);
let result : Array< let result : Array<
{ {
calendar_id : _zeitbild.type.calendar_id; calendar_id : _zeitbild.type.calendar_id;
calendar_name : string;
event : _zeitbild.type.event_object; event : _zeitbild.type.event_object;
} }
> = []; > = [];
for await (const calendar_id of calendar_ids) { for await (const calendar_id of calendar_ids) {
const calendar_object : _zeitbild.type.calendar_object = await _zeitbild.repository.calendar.read( const calendar_object : _zeitbild.type.calendar_object = await _zeitbild.repository.calendar.read(calendar_id);
calendar_id const resource_object : _zeitbild.type.resource_object = await _zeitbild.repository.resource.read(calendar_object.resource_id);
); switch (resource_object.kind) {
if (calendar_object.private) { case "local": {
lib_plankton.log.info(
"calendar_gather_events_private_calendar_blocked",
{
"calendar_id": calendar_id,
}
);
}
else {
switch (calendar_object.kind) {
case "concrete": {
result = ( result = (
result result
.concat( .concat(
calendar_object.data.events resource_object.data.events
.filter( .filter(
(event : _zeitbild.type.event_object) => _zeitbild.helpers.pit_is_between( (event : _zeitbild.type.event_object) => _zeitbild.helpers.pit_is_between(
_zeitbild.helpers.pit_from_datetime(event.begin), _zeitbild.helpers.pit_from_datetime(event.begin),
@ -100,8 +81,7 @@ namespace _zeitbild.service.calendar
.map( .map(
(event : _zeitbild.type.event_object) => ({ (event : _zeitbild.type.event_object) => ({
"calendar_id": calendar_id, "calendar_id": calendar_id,
"calendar_name": calendar_object.name, "event": event,
"event": event
}) })
) )
) )
@ -109,8 +89,9 @@ namespace _zeitbild.service.calendar
break; break;
} }
case "caldav": { case "caldav": {
// TODO readonly
const url : lib_plankton.url.type_url = lib_plankton.url.decode( const url : lib_plankton.url.type_url = lib_plankton.url.decode(
calendar_object.data.source_url calendar_object.data.url
); );
const http_request : lib_plankton.http.type_request = { const http_request : lib_plankton.http.type_request = {
"version": "HTTP/2", "version": "HTTP/2",
@ -189,7 +170,6 @@ namespace _zeitbild.service.calendar
.map( .map(
(event) => ({ (event) => ({
"calendar_id": calendar_id, "calendar_id": calendar_id,
"calendar_name": calendar_object.name,
"event": event, "event": event,
}) })
) )
@ -197,6 +177,11 @@ namespace _zeitbild.service.calendar
); );
break; break;
} }
default: {
return Promise.reject(
new Error("invalid resource kind: " + resource_object["kind"])
);
break;
} }
} }
} }

View file

@ -6,7 +6,9 @@ namespace _zeitbild.type
/** /**
*/ */
type role = ( export type role = (
"admin"
|
"editor" "editor"
| |
"viewer" "viewer"
@ -15,21 +17,16 @@ namespace _zeitbild.type
/** /**
*/ */
type user_id = int; export type user_id = int;
/** /**
*/ */
type user_object = { export type user_object = {
name : string; name : string;
}; };
/**
*/
export type event_id = int;
/** /**
*/ */
export type event_object = { export type event_object = {
@ -55,28 +52,18 @@ namespace _zeitbild.type
/** /**
*/ */
export type calendar_id = int; export type resource_id = int;
/** /**
*/ */
export type calendar_object = ( export type resource_object = (
{ {
name : string; kind : "local";
private : boolean;
}
&
(
{
kind : "concrete";
data : { data : {
users : Array< events : Array<
{ event_object
id : user_id;
role : role;
}
>; >;
events : Array<event_object>;
}; };
} }
| |
@ -84,10 +71,48 @@ namespace _zeitbild.type
kind : "caldav"; kind : "caldav";
data : { data : {
read_only : boolean; read_only : boolean;
source_url : string; url : string;
};
} }
}
)
); );
/**
*/
export type calendar_id = int;
/**
*/
export type calendar_object = {
name : string;
public : boolean;
members : Array<
{
user_id : user_id;
role : role;
}
>;
// resource : resource_object;
resource_id : resource_id;
};
/**
*/
export type root = {
users : Array<
{
id : user_id;
object : user_object;
}
>;
calendars : Array<
{
id : calendar_id;
object : calendar_object;
}
>;
};
} }

View file

@ -26,6 +26,7 @@ ${dir_temp}/zeitbild-unlinked.js: \
${dir_source}/conf.ts \ ${dir_source}/conf.ts \
${dir_source}/database.ts \ ${dir_source}/database.ts \
${dir_source}/types.ts \ ${dir_source}/types.ts \
${dir_source}/repositories/resource.ts \
${dir_source}/repositories/calendar.ts \ ${dir_source}/repositories/calendar.ts \
${dir_source}/services/calendar.ts \ ${dir_source}/services/calendar.ts \
${dir_source}/api/base.ts \ ${dir_source}/api/base.ts \