diff --git a/data/example.json b/data/example.json index c07cbbc..5ea2f80 100644 --- a/data/example.json +++ b/data/example.json @@ -23,13 +23,15 @@ { "id": 1, "name": "house", - "public": false, - "members": [ - { - "user_id": 1, - "role": "editor" - } - ], + "access": { + "default_level": "none", + "attributed": [ + { + "user_id": 1, + "level": "edit" + } + ] + }, "resource": { "kind": "local", "data": { @@ -86,17 +88,19 @@ { "id": 2, "name": "turf", - "public": false, - "members": [ - { - "user_id": 1, - "role": "viewer" - }, - { - "user_id": 2, - "role": "editor" - } - ], + "access": { + "default_level": "none", + "attributed": [ + { + "user_id": 1, + "level": "view" + }, + { + "user_id": 2, + "level": "edit" + } + ] + }, "resource": { "kind": "local", "data": { @@ -138,21 +142,23 @@ { "id": 3, "name": "town", - "public": true, - "members": [ - { - "user_id": 1, - "role": "viewer" - }, - { - "user_id": 2, - "role": "viewer" - }, - { - "user_id": 3, - "role": "editor" - } - ], + "access": { + "default_level": "none", + "attributed": [ + { + "user_id": 1, + "level": "view" + }, + { + "user_id": 2, + "level": "view" + }, + { + "user_id": 3, + "level": "edit" + } + ] + }, "resource": { "kind": "local", "data": { diff --git a/source/api/actions/calendar_add.ts b/source/api/actions/calendar_add.ts new file mode 100644 index 0000000..7718c1a --- /dev/null +++ b/source/api/actions/calendar_add.ts @@ -0,0 +1,50 @@ + +namespace _zeitbild.api +{ + + /** + */ + export function register_calendar_list( + rest_subject : lib_plankton.rest.type_rest + ) : void + { + register< + { + name : string; + public : boolean; + members : Array< + { + user_id : user_id; + role : role; + } + >; + resource_id : resource_id; + }, + int + >( + rest_subject, + lib_plankton.http.enum_method.post, + "/calendar/add", + { + "description": "erstellt einen Kalender", + "output_schema": () => ({ + "nullable": true, + "type": "integer", + }), + "restriction": restriction_logged_in, + "execution": async (stuff) => { + return ( + _zeitbild.service.calendar.overview(user_id) + .then( + data => Promise.resolve({ + "status_code": 200, + "data": data, + }) + ) + ); + } + } + ); + } + +} diff --git a/source/api/actions/calendar_list.ts b/source/api/actions/calendar_list.ts index 9dc4d21..494da2e 100644 --- a/source/api/actions/calendar_list.ts +++ b/source/api/actions/calendar_list.ts @@ -12,10 +12,9 @@ namespace _zeitbild.api null, Array< { - id : _zeitbild.type.calendar_id; + id : int; name : string; - public : boolean; - role : (null | _zeitbild.type.role); + access_level : string; } > >( @@ -41,13 +40,10 @@ namespace _zeitbild.api "type": "string", "nullable": false, }, - "public": { - "type": "boolean", - "nullable": false, - }, - "role": { + "access_level": { "type": "string", "nullable": true, + "enum": ["none", "edit", "view", "admin"] }, }, "required": [ @@ -66,7 +62,19 @@ namespace _zeitbild.api return ( _zeitbild.service.calendar.overview(user_id) .then( - data => Promise.resolve({ + (data_raw) => Promise.resolve( + data_raw + .map( + (entry) => ({ + "id": entry.id, + "name": entry.name, + "access_level": _zeitbild.value_object.access_level.to_string(entry.access_level), + }) + ) + ) + ) + .then( + (data) => Promise.resolve({ "status_code": 200, "data": data, }) diff --git a/source/api/actions/events.ts b/source/api/actions/events.ts index 37725c7..a5661c4 100644 --- a/source/api/actions/events.ts +++ b/source/api/actions/events.ts @@ -142,9 +142,8 @@ namespace _zeitbild.api ) ) ) - // @ts-ignore - .toSorted() ); + calendar_ids.sort(); const data = await _zeitbild.service.calendar.gather_events( calendar_ids, from, diff --git a/source/main.ts b/source/main.ts index cc5846c..aedad07 100644 --- a/source/main.ts +++ b/source/main.ts @@ -14,13 +14,15 @@ type type_data = { { id : int; name : string; - public : boolean; - members : Array< - { - user_id : int; - role : _zeitbild.type.role; - } - >; + access : { + default_level : ("none" | "view" | "edit" | "admin"); + attributed : Array< + { + user_id : int; + level : ("none" | "view" | "edit" | "admin"); + } + >; + }; resource : _zeitbild.type.resource_object; } >; @@ -47,11 +49,12 @@ async function data_init( "calendar": {}, }; for await (const user_raw of data.users) { + const user_object : _zeitbild.type.user_object = { + "name": user_raw.name, + "email_address": user_raw.email_address, + }; const user_id : _zeitbild.type.user_id = await _zeitbild.service.user.add( - { - "name": user_raw.name, - "email_address": user_raw.email_address, - } + user_object ); await _zeitbild.service.auth_internal.set( user_raw.name, @@ -60,24 +63,35 @@ async function data_init( track.user[user_raw.id] = user_id; } for await (const calendar_raw of data.calendars) { + const resource_object : _zeitbild.type.resource_object = calendar_raw.resource; const resource_id : _zeitbild.type.resource_id = await _zeitbild.service.resource.add( - calendar_raw.resource + resource_object ); - const calendar_id : _zeitbild.type.calendar_id = await _zeitbild.service.calendar.add( - { - "name": calendar_raw.name, - "public": calendar_raw.public, - "members": ( - calendar_raw.members - .map( - (member_raw) => ({ - "user_id": track.user[member_raw.user_id], - "role": member_raw.role, - }) + const calendar_object : _zeitbild.type.calendar_object = { + "name": calendar_raw.name, + "access": { + "default_level": _zeitbild.value_object.access_level.from_string(calendar_raw.access.default_level), + "attributed": lib_plankton.map.hashmap.implementation_map( + lib_plankton.map.hashmap.make( + x => x.toFixed(0), + { + "pairs": ( + calendar_raw.access.attributed + .map( + (entry) => ({ + "key": track.user[entry.user_id], + "value": _zeitbild.value_object.access_level.from_string(entry.level), + }) + ) + ), + } ) ), - "resource_id": resource_id, - } + }, + "resource_id": resource_id, + }; + const calendar_id : _zeitbild.type.calendar_id = await _zeitbild.service.calendar.add( + calendar_object ); track.calendar[calendar_raw.id] = calendar_id; } diff --git a/source/repositories/calendar.ts b/source/repositories/calendar.ts index af7561e..df4943d 100644 --- a/source/repositories/calendar.ts +++ b/source/repositories/calendar.ts @@ -9,7 +9,7 @@ namespace _zeitbild.repository.calendar string, any >; - member_rows : Array< + access_attributed_rows : Array< Record< string, any @@ -35,7 +35,7 @@ namespace _zeitbild.repository.calendar /** */ - var _member_chest : ( + var _access_attributed_chest : ( null | lib_plankton.storage.type_chest< @@ -77,7 +77,7 @@ namespace _zeitbild.repository.calendar /** */ - function get_member_chest( + function get_access_attributed_chest( ) : lib_plankton.storage.type_chest< Array, Record, @@ -86,11 +86,11 @@ namespace _zeitbild.repository.calendar Record > { - if (_member_chest === null) { - _member_chest = lib_plankton.storage.sql_table_common.chest( + if (_access_attributed_chest === null) { + _access_attributed_chest = lib_plankton.storage.sql_table_common.chest( { "database_implementation": _zeitbild.database.get_implementation(), - "table_name": "calendar_members", + "table_name": "calendar_access_attributed", "key_names": ["calendar_id","user_id"], } ); @@ -98,7 +98,41 @@ namespace _zeitbild.repository.calendar else { // do nothing } - return _member_chest; + return _access_attributed_chest; + } + + + /** + */ + function encode_access_level( + access_level : _zeitbild.type.enum_access_level + ) : int + { + return ( + [ + _zeitbild.type.enum_access_level.none, + _zeitbild.type.enum_access_level.view, + _zeitbild.type.enum_access_level.edit, + _zeitbild.type.enum_access_level.admin, + ].indexOf(access_level) + ); + } + + + /** + */ + function decode_access_level( + access_level_encoded : int + ) : _zeitbild.type.enum_access_level + { + return ( + [ + _zeitbild.type.enum_access_level.none, + _zeitbild.type.enum_access_level.view, + _zeitbild.type.enum_access_level.edit, + _zeitbild.type.enum_access_level.admin, + ][access_level_encoded] + ); } @@ -111,16 +145,16 @@ namespace _zeitbild.repository.calendar return { "core_row": { "name": object.name, - "public": object.public, + "access_level_default": encode_access_level(object.access.default_level), "resource_id": object.resource_id, }, - "member_rows": ( - object.members + "access_attributed_rows": ( + lib_plankton.map.dump(object.access.attributed) .map( - (member) => ({ + ({"key": user_id, "value": level}) => ({ // "calendar_id": calendar_id, - "user_id": member.user_id, - "role": member.role, + "user_id": user_id, + "level": encode_access_level(level), }) ) ), @@ -136,17 +170,19 @@ namespace _zeitbild.repository.calendar { return { "name": dispersal.core_row["name"], - "public": dispersal.core_row["public"], - "members": ( - dispersal.member_rows - .map( - (member_row) => ({ - "calendar_id": member_row["calendar_id"], - "user_id": member_row["user_id"], - "role": member_row["role"], - }) - ) - ), + "access": { + "default_level": decode_access_level(dispersal.core_row["access_level_default"]), + "attributed": Object.fromEntries( + dispersal.access_attributed_rows + .map( + (access_attributed_row) => ([ + // "calendar_id": access_attributed_row["calendar_id"], + access_attributed_row["user_id"], + decode_access_level(access_attributed_row["level"]), + ]) + ) + ), + }, "resource_id": dispersal.core_row["resource_id"], }; } @@ -213,7 +249,7 @@ namespace _zeitbild.repository.calendar get_core_store().read(id) .then( (core_row) => ( - get_member_chest().search( + get_access_attributed_chest().search( { "expression": "(calendar_id = $calendar_id)", "arguments": { @@ -222,10 +258,10 @@ namespace _zeitbild.repository.calendar } ) .then( - (member_rows) => Promise.resolve( + (access_attributed_rows) => Promise.resolve( { "core_row": core_row, - "member_rows": member_rows, + "access_attributed_rows": access_attributed_rows, } ) ) @@ -257,11 +293,11 @@ namespace _zeitbild.repository.calendar .then<_zeitbild.type.calendar_id>( (calendar_id) => ( Promise.all( - dispersal.member_rows + dispersal.access_attributed_rows .map( - (member_row) => get_member_chest().write( - [calendar_id, member_row["user_id"]], - {"role": member_row["role"]} + (access_attributed_row) => get_access_attributed_chest().write( + [calendar_id, access_attributed_row["user_id"]], + {"level": access_attributed_row["level"]} ) ) ) @@ -277,51 +313,43 @@ namespace _zeitbild.repository.calendar /** - * @todo sort for role, public and id */ - export function overview( + export async function overview( user_id : _zeitbild.type.user_id ) : Promise< Array< { id : _zeitbild.type.calendar_id; name : string; - public : boolean; - role : (null | _zeitbild.type.role); + access_level : _zeitbild.type.enum_access_level; } > > { return ( - _zeitbild.database.get_implementation().query_free_get( - { - "template": "SELECT x.id AS id, x.name AS name, x.public AS public, MAX(y.role) AS role FROM calendars AS x LEFT OUTER JOIN calendar_members AS y ON (x.id = y.calendar_id) WHERE (x.public OR (y.user_id = $user_id)) GROUP BY x.id;", - "arguments": { - "user_id": user_id, + lib_plankton.file.read("sql/calendar_overview.sql") + .then( + (template) => _zeitbild.database.get_implementation().query_free_get( + { + "template": template, + "arguments": { + "user_id": user_id, + } } - } + ) ) .then( (rows) => Promise.resolve( - rows.map( + rows + .map( (row) => ({ "id": row["id"], "name": row["name"], - "public": (row["public"] === 1), - "role": row["role"], + "access_level": decode_access_level(row["access_level"]), }) ) ) ) - /* - .then( - (entries) => Promise.resolve( - entries.toSorted( - (x, y) => (x - ) - ) - ) - */ ) } } diff --git a/source/repositories/sql/calendar_overview.sql b/source/repositories/sql/calendar_overview.sql new file mode 100644 index 0000000..7fb2808 --- /dev/null +++ b/source/repositories/sql/calendar_overview.sql @@ -0,0 +1,20 @@ +SELECT + x.id AS id, + x.name AS name, + ( + CASE + WHEN MAX(y.level) IS NULL THEN x.access_level_default + ELSE MAX(y.level) + END + ) AS access_level +FROM + calendars AS x + LEFT OUTER JOIN calendar_access_attributed AS y ON ((x.id = y.calendar_id) AND (y.user_id = $user_id)) +GROUP BY + x.id +HAVING + (access_level > 0) +ORDER BY + access_level, + id +; diff --git a/source/services/calendar.ts b/source/services/calendar.ts index 48d3da2..3866d8b 100644 --- a/source/services/calendar.ts +++ b/source/services/calendar.ts @@ -50,8 +50,7 @@ namespace _zeitbild.service.calendar { id : _zeitbild.type.calendar_id; name : string; - public : boolean; - role : (null | _zeitbild.type.role); + access_level : _zeitbild.type.enum_access_level; } > > diff --git a/source/types.ts b/source/types.ts index 54c48db..c73fcd7 100644 --- a/source/types.ts +++ b/source/types.ts @@ -6,13 +6,12 @@ namespace _zeitbild.type /** */ - export type role = ( - "admin" - | - "editor" - | - "viewer" - ); + export enum enum_access_level { + none, + view, + edit, + admin + } /** @@ -91,13 +90,13 @@ namespace _zeitbild.type */ export type calendar_object = { name : string; - public : boolean; - members : Array< - { - user_id : user_id; - role : role; - } - >; + access : { + default_level : enum_access_level; + attributed : lib_plankton.map.type_map< + user_id, + enum_access_level + >; + }; resource_id : resource_id; }; diff --git a/source/value_objects/access_level.ts b/source/value_objects/access_level.ts new file mode 100644 index 0000000..6c97d58 --- /dev/null +++ b/source/value_objects/access_level.ts @@ -0,0 +1,55 @@ + +/** + */ +namespace _zeitbild.value_object.access_level +{ + + /** + */ + export function to_string( + access_level : _zeitbild.type.enum_access_level + ) : string + { + switch (access_level) { + case _zeitbild.type.enum_access_level.none: {return "none";} + case _zeitbild.type.enum_access_level.view: {return "view";} + case _zeitbild.type.enum_access_level.edit: {return "edit";} + case _zeitbild.type.enum_access_level.admin: {return "admin";} + default: {throw (new Error("invalid access level: " + String(access_level)));} + } + } + + + /** + */ + export function from_string( + access_level_ : string + ) : _zeitbild.type.enum_access_level + { + switch (access_level_) { + case "none": {return _zeitbild.type.enum_access_level.none;} + case "view": {return _zeitbild.type.enum_access_level.view;} + case "edit": {return _zeitbild.type.enum_access_level.edit;} + case "admin": {return _zeitbild.type.enum_access_level.admin;} + default: {throw (new Error("invalid encoded access level: " + String(access_level_)));} + } + } + + + /** + */ + export function order( + x : _zeitbild.type.enum_access_level, + y : _zeitbild.type.enum_access_level + ) : boolean + { + const list : Array<_zeitbild.type.enum_access_level> = [ + _zeitbild.type.enum_access_level.none, + _zeitbild.type.enum_access_level.view, + _zeitbild.type.enum_access_level.edit, + _zeitbild.type.enum_access_level.admin, + ]; + return (list.indexOf(x) <= list.indexOf(y)); + } + +} diff --git a/tools/makefile b/tools/makefile index 24533e2..8e61c4e 100644 --- a/tools/makefile +++ b/tools/makefile @@ -18,11 +18,14 @@ cmd_tsc := ${dir_tools}/typescript/node_modules/.bin/tsc ## rules .PHONY: default -default: ${dir_build}/zeitbild node_modules +default: sql ${dir_build}/zeitbild node_modules -.PHONY: node_modules -node_modules: - @ cd ${dir_build} && npm install sqlite3 bcrypt +.PHONY: sql +sql: \ + $(wildcard ${dir_source}/repositories/sql/*) + @ ${cmd_log} "sql …" + @ ${cmd_mkdir} ${dir_build}/sql + @ ${cmd_cp} -r -u $^ ${dir_build}/sql/ ${dir_temp}/conf.ts: \ ${dir_source}/conf.ts.tpl \ @@ -37,6 +40,7 @@ ${dir_temp}/zeitbild-unlinked.js: \ ${dir_source}/database.ts \ ${dir_source}/auth.ts \ ${dir_source}/types.ts \ + ${dir_source}/value_objects/access_level.ts \ ${dir_source}/repositories/auth_internal.ts \ ${dir_source}/repositories/user.ts \ ${dir_source}/repositories/resource.ts \ @@ -72,3 +76,7 @@ ${dir_build}/zeitbild: \ @ ${cmd_mkdir} $(dir $@) @ ${cmd_cat} $^ > $@ @ ${cmd_chmod} +x $@ + +.PHONY: node_modules +node_modules: + @ cd ${dir_build} && npm install sqlite3 bcrypt