clean up docs that are in the wiki
This commit is contained in:
parent
610a10712d
commit
65bd70375d
@ -1 +0,0 @@
|
||||
<mxfile host="www.draw.io" modified="2020-01-05T12:52:00.802Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36" etag="kaO24BhHNubXhychog86" version="12.4.8" type="device"><diagram id="28u5DdNZLLq8z4gjoHRL" name="Page-1">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</diagram></mxfile>
|
Binary file not shown.
Before Width: | Height: | Size: 83 KiB |
@ -1,53 +0,0 @@
|
||||
# Documentation for the streaming Client-Server-Protocol
|
||||
|
||||
0) Notation
|
||||
Special words with a definied meaning are surrounded by underscores. E.g. the
|
||||
word _request_ would be the unique term for the http-message send form the
|
||||
client to the server.
|
||||
|
||||
1) What?
|
||||
This document provides the specification for the unidirectional server to client
|
||||
streaming. This is realized with HTTP1.1 streaming responses. The general
|
||||
pattern starts with an _initial request_ form the client to a streaming endpoint.
|
||||
The server can send _packets_ of data to the client whenever the server wants to.
|
||||
The connection stays open, until one side closes it manually.
|
||||
|
||||
2) Production usage
|
||||
To keep the amount of tcp connections, connection establishments and the
|
||||
associated overhead low, it is advised to ensure HTTP2 in production to make use
|
||||
out of tcp multiplexing. All streaming (and other non-streaming) content is
|
||||
multiplexed through a single tcp connection.
|
||||
|
||||
3) Request meta information
|
||||
The method, headers and payload of the initiating request are arbitrary and not
|
||||
bounded to an restriction. The response content type header must be set to
|
||||
`application/octet-stream` to avoid unwanted buffering in some browsers.
|
||||
Otherwise all other headers are free to choose. If errors happen before the
|
||||
response was send, the response status code can be a non-200 one.
|
||||
|
||||
4) Message format: Packets
|
||||
The server sends JSON objects (with an object as the outer most JSON-object) as
|
||||
the _content_ of a package. So one package contains one JSON object. The content
|
||||
is terminated with a line feed (0x0A). The terminated content is the _payload_,
|
||||
which is send over the connection. It must be taken care of not sending
|
||||
prettified JSON, to not include any unwanted line feeds and to not send
|
||||
unnecessary bytes. To clearify, the terminating line feed must be the only line
|
||||
feed in the payload. Note that linefeeds must be escaped in JSON-strings (see
|
||||
RFC 7159).
|
||||
|
||||
5) Errors
|
||||
If an error happen in an established connection, the status code can not be
|
||||
altered. To indicate errors, the following convention is used: To send an error
|
||||
to the client, send a JSON-object with the single key `error` with an object as
|
||||
value (e.g. {"error": {"detail": "Failed successfully"}}`). The server must
|
||||
close the connection afterwards, because the client cannot react to the error.
|
||||
For normal packets, it is forbidden to send content with an error key in the
|
||||
outer most JSON-object.
|
||||
|
||||
5) Outlook
|
||||
There might be an additional reserved keys. The protocol may be extended to:
|
||||
- a watchdog: the server sends a ping in regular time intervals. The client
|
||||
take note of them and notices, if there was a missing ping. E.g. the timeout
|
||||
for the client is twice the send interval time. If a missing ping was
|
||||
detected, the connection is reestablished.
|
||||
|
1
docs/datavalidator/export.json
Normal file
1
docs/datavalidator/export.json
Normal file
File diff suppressed because one or more lines are too long
@ -1,80 +0,0 @@
|
||||
// Actions Service Interface
|
||||
|
||||
/**
|
||||
* Executes multiple actions in the context of the user given by the user_id.
|
||||
* There are two modes of execution:
|
||||
* atomic=true (default):
|
||||
* All actions are validated in common, so if one action or one payload of
|
||||
* one action fails, the request is aborted with an ActionException indicating
|
||||
* the problematic action with both indices.
|
||||
* => The whole request is handled atomically.
|
||||
* atomic=false:
|
||||
* Each action is validated by it's own. If there is an error, the error must
|
||||
* be reported via an ActionError in the ActionsResponse. The actions result
|
||||
* is not written into the Datastore. It might raise an ActionException if the
|
||||
* single write request to the Datastore fails (e.g. because of locking there)
|
||||
*
|
||||
* For general, non specific action-related, errors an ActionException is used.
|
||||
*
|
||||
* @throws ActionException
|
||||
*/
|
||||
handle_request(payload: Action[], user_id: Id, atomic?: boolean): ActionsResponse
|
||||
|
||||
interface Action {
|
||||
action: string;
|
||||
data: object[];
|
||||
}
|
||||
|
||||
interace ActionsResponse {
|
||||
success: true;
|
||||
message: string;
|
||||
|
||||
/**
|
||||
* This is a potentially double-array with entries for each action
|
||||
* and, if not null, an array for each data provided for each action.
|
||||
*
|
||||
* If an action does not produce a result, the inner array can be omitted and
|
||||
* null can be used. If the inner array is given, each entry must be an object
|
||||
* with the result (e.g. for a create action `{id: <the id>}`) or null.
|
||||
*
|
||||
* E.g. for valid arrays (two actions with two data entries each):
|
||||
* - [null, null]
|
||||
* - [null, [null, null]]
|
||||
* - [null, [{id: 2}, null]]
|
||||
* - [[{id: 2}, {id: 3}], [{id: 5}, {id: 8}]]
|
||||
*
|
||||
*
|
||||
* To report errors, use the ActionError format!
|
||||
**/
|
||||
results: (object[] | ActionError | null )[]
|
||||
}
|
||||
|
||||
/**
|
||||
* If action_data_error_index is given, the error can be directly associated with the
|
||||
* respective action data. If not, the error is of general fashion and/or not directly
|
||||
* associated with a single action data.
|
||||
*
|
||||
* Note: ActionError can only be used if atomic=false.
|
||||
*/
|
||||
interface ActionError {
|
||||
success: false;
|
||||
message: string;
|
||||
action_data_error_index?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON resonse with a status code of !=200. If a specific action raised the error,
|
||||
* use the action_error_index and action_data_error_index to indicate the errored
|
||||
* action and data, respectively. If there were general errors, both indices must be
|
||||
* omitted or null.
|
||||
*
|
||||
* If the atomic flag was false in the request, it is not allowed to send
|
||||
* action-specific errors with this exception. It must be responded with an
|
||||
* ActionError through ActionsResponse (resulting in a status code of 200).
|
||||
*/
|
||||
Exception ActionException {
|
||||
success: false;
|
||||
message: string;
|
||||
action_error_index?: number;
|
||||
action_data_error_index?: number;
|
||||
}
|
@ -1,131 +0,0 @@
|
||||
// Description of the authentication-service
|
||||
// It is listening on port '9004'
|
||||
// Routes with a prefix 'secure' are protected routes, that can only accessed with a valid ticket.
|
||||
// That are routes, that call internally 'authenticate'
|
||||
|
||||
|
||||
// The properties of this interface have to be passed as HTTP-headers in a request.
|
||||
Interface Ticket {
|
||||
authentication: string,
|
||||
cookies: {
|
||||
refreshId: string,
|
||||
[name: string]: string
|
||||
}
|
||||
}
|
||||
|
||||
// This describes, which information is received by requesting `secure/authenticate`.
|
||||
Interface LoginInformation {
|
||||
userId: number;
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes an http-response, which is sent back to any requesting service.
|
||||
*/
|
||||
Interface Response <T> {
|
||||
set-authentication-header: string // If an old access-token is expired and refreshed, it is set as authentication-header.
|
||||
// This determines if a request was successful.
|
||||
success: boolean,
|
||||
// This sends back a describing message. For example, the reason of a failured request.
|
||||
message: string,
|
||||
// Optional data, which is appended, if a request was successful.
|
||||
data?: T
|
||||
}
|
||||
|
||||
/**
|
||||
* The credentials for login/authentication are not valid.
|
||||
*/
|
||||
Exception InvalidCredentials {
|
||||
success: false,
|
||||
message: string
|
||||
}
|
||||
|
||||
/**
|
||||
* POST to /system/auth/login
|
||||
*
|
||||
* A user can login with its credentials for authentication.
|
||||
* If they are correct, the service answers with a signed Token and sets a cookie, containing the sessionId of the client.
|
||||
*
|
||||
* If they aren't correct, the service throws an error.
|
||||
*
|
||||
* @throws InvalidCredentials
|
||||
*/
|
||||
login (username: string, password: string): Response<void>;
|
||||
|
||||
/**
|
||||
* POST to /internal/auth/authenticate
|
||||
*
|
||||
* This will be a library to act as part of the auth-service. The other services have not to
|
||||
* request the auth-service for authentication. Instead, they use this library-function in their own
|
||||
* and receive knowledge about authentication without request.
|
||||
*
|
||||
* Throws an exception, if the token is not valid. E.g. if the signature is not valid.
|
||||
*
|
||||
* @throws InvalidCredentials
|
||||
*/
|
||||
authenticate (ticket: Ticket): Response<LoginInformation>;
|
||||
|
||||
/**
|
||||
* POST to /system/auth/who-am-i
|
||||
*
|
||||
* A request to get knowledge about themselves. This information is contained in the payload of
|
||||
* a Token. So, this function handles the refreshing of a Token.
|
||||
* Expects a jwt as string in a cookie (called 'refreshId').
|
||||
*
|
||||
* Sends back a new Token (passed as http-header).
|
||||
*
|
||||
* Throws an exception, if the cookie is empty, the transmitted sessionId is wrong or the signature is wrong.
|
||||
*
|
||||
* @throws InvalidCredentials
|
||||
*/
|
||||
who-am-i (refreshId: string): Response<LoginInformation>;
|
||||
|
||||
/**
|
||||
* POST to /system/auth/secure/clear-session-by-id
|
||||
*
|
||||
* Function to sign out one specific client from a user by its corresponding session-id.
|
||||
*/
|
||||
secure/clear-session-by-id (sessionId: string, ticket: Ticket): Response<void> publishes LogoutSessionEvent;
|
||||
|
||||
/**
|
||||
* POST to /system/auth/secure/clear-all-session-except-themselves
|
||||
*
|
||||
* Function to kill all current opened sessions from one user except the one, which is requesting.
|
||||
*/
|
||||
secure/clear-all-sessions-except-themselves (sessionId: string, ticket: Ticket): Response<void> publishes LogoutSessionEvent;
|
||||
|
||||
/**
|
||||
* POST to /system/auth/secure/logout
|
||||
*
|
||||
* The service deletes the session depending on the given Token.
|
||||
*
|
||||
* @throws InvalidCredentials
|
||||
*/
|
||||
secure/logout (ticket: Ticket): Response<void> publishes LogoutSessionEvent;
|
||||
|
||||
/**
|
||||
* GET to system/auth/secure/list-sessions
|
||||
*
|
||||
* Returns all currently active sessions.
|
||||
*
|
||||
* @returns an array containing currently active sessions.
|
||||
*/
|
||||
secure/list-sessions (ticket: Ticket): Response<{sessions: string[]}>;
|
||||
|
||||
/**
|
||||
* POST to /internal/auth/hash
|
||||
*
|
||||
* Hashes a given value. A random salt (64bit) is generated and added to the hashed value.
|
||||
*
|
||||
* @returns the hashed value. The hashed value is structured as follows: [salt + hash].
|
||||
*/
|
||||
hash (toHash: string): Response<{hash: string}>;
|
||||
|
||||
/**
|
||||
* POST to /internal/auth/is-equals
|
||||
*
|
||||
* Compares a given value with an given hash.
|
||||
*
|
||||
* @returns a boolean, if the hashed value of the given value is equals to the passed hash.
|
||||
*/
|
||||
is-equals (toHash: string, toCompare: string): Response<{isEquals: boolean}>;
|
@ -1,145 +0,0 @@
|
||||
/**
|
||||
* SyntaxError is returned, when the syntax of the request body is wrong.
|
||||
* This error is returned on the beginning of a request with http-status-code
|
||||
* 400.
|
||||
*/
|
||||
Exception SyntaxError(msg: string);
|
||||
|
||||
/**
|
||||
* JsonError is returned, when the body does not contain valid json. This error
|
||||
* is returned on the beginning of a request with http-status-code 400.
|
||||
*/
|
||||
Exception JsonError(msg: string);
|
||||
|
||||
/**
|
||||
* ValueError is returned, when the value of a field does not have the expected
|
||||
* format. E.g. there is an indicated relation to a key, but the data are no
|
||||
* foreign ids/fqids. The exception may happen, if the stream is used at
|
||||
* runtime, because this cannot be detected when the caller makes the request.
|
||||
*/
|
||||
Exception ValueError(msg: string);
|
||||
|
||||
/**
|
||||
* InternalError is an unexpected error on the server side. When it happens at
|
||||
* the beginning of a request, an http-status-code 500 is used. But it can also
|
||||
* happen after the first data have been streamed to the client. The error does
|
||||
* not contain any useful information. More information can be found in the
|
||||
* server log. This is the only error that generates a server log message.
|
||||
*/
|
||||
Expection InternalError(msg: string);
|
||||
|
||||
/**
|
||||
* This methods subscribes to a list of given request. The response is a stream
|
||||
* (language dependent) updating all models according to the ModelRequest if new
|
||||
* data is available. On subscription, initial data must be pushed to the caller
|
||||
* as soon as possible. The stream can be closed by closing the stream (e.g. the
|
||||
* underlying network connection).
|
||||
*
|
||||
* @throws SyntaxError
|
||||
* @throws JsonError
|
||||
* @throws ValueError
|
||||
* @throws InternalError
|
||||
*/
|
||||
subscribe(request: ModelRequest[]): stream<ModelData>;
|
||||
|
||||
/**
|
||||
* This is the main interface for requesting models in a structured, nested way.
|
||||
* The initial request targets some models as the root models of one collection
|
||||
* with all the same fields. This build a tree of dependencies, because the
|
||||
* value of some fields may be a GenericRelationFieldDescriptor again.
|
||||
*
|
||||
* For a description of `fields` and `collection`, see
|
||||
* GenericRelationFieldDescriptor and RelationFieldDescriptor.
|
||||
*
|
||||
* `ids`: This is a list of ids for a collection, that should be provided. All
|
||||
* models, that the user can see, must be included in the response. The model
|
||||
* fields are handled according to `GenericRelationFieldDescriptor`.
|
||||
*/
|
||||
Interface ModelRequest extends Fields {
|
||||
ids: ID[];
|
||||
collection: Collection;
|
||||
}
|
||||
|
||||
Interface Fields {
|
||||
fields: {
|
||||
[field: Field]: GenericRelationFieldDescriptor
|
||||
| RelationFieldDescriptor
|
||||
| StructuredFieldDescriptor
|
||||
| null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For an overview, see `ModelRequest`.
|
||||
*
|
||||
* `fields` (inherited from `Fields`, see above):
|
||||
* Regardless of the value of a field, the restricted values are given in the
|
||||
* response (multiple for structured fields). If the restricted value is null,
|
||||
* the field must be included here.
|
||||
*
|
||||
* If the value is not null, it is indicated, that there is a reference to
|
||||
* follow. There are three types of values:
|
||||
* - GenericRelationFieldDescriptor: The reference is a generic one. This means,
|
||||
* that the actual value from the model is a fqid or an array of fqids.
|
||||
* - RelationFieldDescriptor: A collection is given, so it can be expected, that
|
||||
* the actual model value is an id or an array of ids.
|
||||
* - StructuredFieldDescriptor: The field is a template field and should be
|
||||
* handled in this way.
|
||||
*/
|
||||
Interface GenericRelationFieldDecriptor extends Fields {
|
||||
type: 'generic-relation' | 'generic-relation-list';
|
||||
}
|
||||
|
||||
/**
|
||||
* For an overview, see `ModelRequest`. For `fields`, see
|
||||
* GenericRelationFieldDescriptor.
|
||||
*
|
||||
* `collection`:
|
||||
* This is the collection, the ids are associated to. The ids are provided with
|
||||
* two different ways:
|
||||
* - In a ModelRequest, the ids are given.
|
||||
* - If this interface is used in a field indication a relation, the id(s) are
|
||||
* given by the actual model data.
|
||||
*/
|
||||
Interface RelationFieldDescriptor extends Fields {
|
||||
type: 'relation' | 'relation-list';
|
||||
collection: Collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Structured Fields: A field with `$` as a placeholder can be structured. The
|
||||
* `$` is a template placeholder. The `template field` holds a list of strings
|
||||
* with options to insert into `$`. All these fields are the `structured
|
||||
* fields`. Example:
|
||||
* B_$_ids is the template field. Its value may be `["1", "test"]`, so the
|
||||
* structured fields are B_1_ids and B_test_ids.
|
||||
*
|
||||
* This interface indicates, that there are structured fields related to the
|
||||
* given template field, that should be resolved. If one would give `B_$_ids:
|
||||
* null` without using this interface, the response would be just the template
|
||||
* field `B_$_ids` and no structured fields.
|
||||
*
|
||||
* For just resolving values, leave `values` out (hence the optional parameter).
|
||||
*
|
||||
* For retrieveing a single structured field, use `RelationFieldDescriptor`. E.g.
|
||||
* when trying to get the groups for a user from meeting 2, use `group_2_ids`
|
||||
* directly and do not bother with structured fields. The terminology is a
|
||||
* `specific structured field`.
|
||||
*
|
||||
* If the values of all structured fields are references, use the `values`
|
||||
* parameter analog to the `fields` paramter as documented in
|
||||
* `GenericRelationFieldDescriptor`.
|
||||
*/
|
||||
Interface StructuredFieldDecriptor {
|
||||
type: 'template',
|
||||
values?: GenericRelationFieldDescriptor | RelationFieldDescriptor;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This structure holds all data given by the called service as a map of
|
||||
* fqfields to the fields values.
|
||||
*/
|
||||
Interface ModelData {
|
||||
[fqfield: Fqfield]: Value;
|
||||
}
|
@ -1,332 +0,0 @@
|
||||
# Datastore Interface
|
||||
|
||||
Enum EventType {
|
||||
Create,
|
||||
Update,
|
||||
Delete,
|
||||
Restore,
|
||||
}
|
||||
|
||||
Exception ModelDoesNotExist(model: Fqid);
|
||||
Exception ModelExist(model: Fqid);
|
||||
Exception ModelNotDeleted(model: Fqid);
|
||||
Exception ModelLocked(key: (Fqid | Fqfield | CollectionField)[]);
|
||||
Exception InvalidFormat(msg: string);
|
||||
Exception InvalidRequest(msg: string);
|
||||
|
||||
# Note: Error returns via HTTP 400:
|
||||
Interface ErrorResponse {
|
||||
error: InvalidFormatData |
|
||||
InvalidRequestData |
|
||||
ModelDoesNotExistData |
|
||||
ModelExistData |
|
||||
ModelMotDeletedData |
|
||||
ModelLockedData;
|
||||
}
|
||||
Interface InvalidFormatData {
|
||||
type: 1;
|
||||
msg: string;
|
||||
}
|
||||
Interface InvalidRequestData {
|
||||
type: 2;
|
||||
msg: string;
|
||||
}
|
||||
Interface ModelDoesNotExistData {
|
||||
type: 3;
|
||||
fqid: string;
|
||||
}
|
||||
Interface ModelExistData {
|
||||
type: 4;
|
||||
fqid: string;
|
||||
}
|
||||
Interface ModelNotDeletedData {
|
||||
type: 5;
|
||||
fqid: string;
|
||||
}
|
||||
Interface ModelLockedData {
|
||||
type: 6;
|
||||
keys: string[];
|
||||
}
|
||||
|
||||
## Writer
|
||||
# Note: Different host and port than the reader!
|
||||
|
||||
/**
|
||||
* Writes Events into the datastore.
|
||||
* If multiple WriteRequests are given, they are fully executed one-by-one, meaning
|
||||
* if a earlier event invalidates the locked_field of a later WriteRequest, an
|
||||
* exception is thrown.
|
||||
* Url: POST to /internal/datastore/writer/write
|
||||
*
|
||||
* @throws ModelDoesNotExist
|
||||
* @throws ModelExists
|
||||
* @throws ModelLocked
|
||||
* @throws InvalidFormat
|
||||
* @throws ModelNotDeleted
|
||||
*/
|
||||
write(request: WriteRequest | WriteRequest[]): void publishes ModifiedFieldsEvent
|
||||
|
||||
Interface WriteRequest {
|
||||
events: (CreateEvent | RestoreEvent | UpdateEvent | DeleteEvent)[];
|
||||
information: {
|
||||
<fqid>: Object;
|
||||
};
|
||||
user_id: number;
|
||||
locked_fields: {
|
||||
<fqid>: Position;
|
||||
<fqfield>: Position;
|
||||
<CollectionField>: Position | CollectionFieldLock | CollectionFieldLock[];
|
||||
}
|
||||
}
|
||||
|
||||
Interface CreateEvent {
|
||||
type: 'create';
|
||||
fqid: Fqid;
|
||||
fields: {
|
||||
<field>: Value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: For deleting keys, they must be set to `None`. These keys will be removed from
|
||||
* the model.
|
||||
* list_fields can be used to partially update list fields: the values in `add` will be
|
||||
* appended to the given field, the values in `remove` will be removed from the field.
|
||||
* Either fields or list_fields must be given or an error will be thrown.
|
||||
* An exception will be thrown if:
|
||||
* - a field in list_fields is not empty and not a list
|
||||
* - a field in list_fields contains other entries than strings or ints
|
||||
* Other edge cases:
|
||||
* - an element should be added that is already in the list: this element is ignored,
|
||||
* other potentially given elements are still added as normal
|
||||
* - an element should be removed that is not in the list: this element is ignored,
|
||||
* other potentially given elements are still removed as normal
|
||||
* - the field does not yet exist on the model:
|
||||
* - add: same function as if the value was given in `fields`
|
||||
* - remove: nothing happens
|
||||
*/
|
||||
Interface UpdateEvent {
|
||||
type: 'update';
|
||||
fqid: Fqid;
|
||||
fields: {
|
||||
<field>: Value;
|
||||
}
|
||||
list_fields: {
|
||||
add: {
|
||||
<field>: Value[];
|
||||
}
|
||||
remove: {
|
||||
<field>: Value[];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Interface RestoreEvent {
|
||||
type: 'restore';
|
||||
fqid: Fqid;
|
||||
}
|
||||
|
||||
Interface DeleteEvent {
|
||||
type: 'delete';
|
||||
fqid: Fqid;
|
||||
}
|
||||
|
||||
// Collection fields can not only be locked to a specific position, but also filtered
|
||||
// first, e.g. when selecting all models from a specific meeting. WARNING: the filter
|
||||
// should always contain an equals check with the meeting_id, since this will be
|
||||
// indexed. Other filters can lead to long query times.
|
||||
// If no filter is given, it has the same meaning as just giving the position.
|
||||
Interface CollectionFieldLock {
|
||||
position: Position;
|
||||
filter: Filter | null;
|
||||
}
|
||||
|
||||
// Note: The modified fqfields include:
|
||||
// - all updated fqfields
|
||||
// - all deleted fqfields
|
||||
// - all fqfields of all deleted models
|
||||
// - all fqfields of all created models
|
||||
// - all fqfields of all restored models (logically the same as created)
|
||||
Event ModifiedFieldsEvent on topic ModifiedFields {
|
||||
modified: Fqfield[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reserves multiple sequential ids for the given collection und returns them.
|
||||
* Url: POST to /internal/datastore/writer/reserve_ids
|
||||
*/
|
||||
reserveIds(collection: Collection, amount: number): Id[]
|
||||
|
||||
|
||||
## Reader
|
||||
# Note: Different host and port than the writer!
|
||||
|
||||
/** Common notes:
|
||||
* - parameter `position`: Optional, if given reads the data to this position.
|
||||
* - parameter `mapped_fields`: List of fields that should only be present in the response.
|
||||
* - parameter `get_deleted_models`: Optional, defines which models to return
|
||||
* - DeletedModelsBehaviour.NO_DELETED: (Default) only non-deleted models are returned.
|
||||
* get throws a ModelDoesNotExist error if the given
|
||||
* model is deleted.
|
||||
* - DeletedModelsBehaviour.ONLY_DELETED: only deleted models are returned. get throws
|
||||
* a ModelNotDeleted if the given model is not deleted.
|
||||
* - DeletedModelsBehaviour.ALL_MODELS: all models are returned
|
||||
* - All operations adds the fields `meta_position` and `meta_deleted` to the models.
|
||||
* - The InvalidFormat exception can always be thrown, if the requested formats are
|
||||
* wrong, including something like empty collections, ...
|
||||
*/
|
||||
|
||||
Enum DeletedModelsBehaviour {
|
||||
NO_DELETED = 1,
|
||||
ONLY_DELETED = 2,
|
||||
ALL_MODELS = 3
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a model by fqid.
|
||||
* Url: POST to /internal/datastore/reader/get
|
||||
*
|
||||
* @throws ModelDoesNotExist
|
||||
* @throws InvalidFormat
|
||||
*/
|
||||
get(fqid: Fqid, mapped_fields?: Field[], position?: Position, get_deleted_models?: DeletedModelsBehaviour): Partial<Model>;
|
||||
|
||||
/**
|
||||
* Returns multiple models.
|
||||
* Url: POST to /internal/datastore/reader/get_many
|
||||
*
|
||||
* Can either be called with a list of Fqfields or with a list of specific request
|
||||
* objects that map a collection to the needed ids and fields. If both the lower and
|
||||
* the higher level mapped_fields are given, the higher level one is merged into all
|
||||
* lower level ones. If Fqfields are given, the mapped_fields are ignored.
|
||||
* If an id is not found, it is not included in the response instead of throwing a
|
||||
* ModelDoesNotExist.
|
||||
*
|
||||
* @returns A mapping of collection to ids to models. Example:
|
||||
* {
|
||||
* "collection1": {
|
||||
* "id1": {
|
||||
* "field1": "foo",
|
||||
* "field2": "bar",
|
||||
* },
|
||||
* },
|
||||
* "collection2": {
|
||||
* "id2": {
|
||||
* "field3": 42,
|
||||
* },
|
||||
* },
|
||||
* }
|
||||
*
|
||||
* @throws InvalidFormat
|
||||
*/
|
||||
get_many(requests: GetManyRequest[] | Fqfield[], mapped_fields?: Field[], position?: Position, get_deleted_models?: DeletedModelsBehaviour): Map<Collection, Map<Id, Partial<Model>>>;
|
||||
|
||||
Interface GetManyRequest {
|
||||
collection: Collection;
|
||||
ids: Id[];
|
||||
mapped_fields?: Field[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all models of one collection.
|
||||
* Url: POST to /internal/datastore/reader/get_all
|
||||
*
|
||||
* It is not possible to specify a position, so this method cannot be used if the user
|
||||
* browses the history. It should be noted that it is highly disencouraged to use this
|
||||
* method because it might return a huge amount of data.
|
||||
*
|
||||
* @returns see get_many
|
||||
* @throws InvalidFormat
|
||||
*/
|
||||
get_all(collection: Collection, mapped_fields?: Field[], get_deleted_models?: DeletedModelsBehaviour): Map<Id, Partial<Model>>;
|
||||
|
||||
/**
|
||||
* Returns all models.
|
||||
* Url: POST to /internal/datastore/reader/get_everything
|
||||
*
|
||||
* This is a dev route only!
|
||||
*
|
||||
* @returns The example data format: A mapping of a collection to a list of models.
|
||||
*/
|
||||
get_everything(get_deleted_models?: DeletedModelsBehaviour): Map<Collection, Model[]>;
|
||||
|
||||
interface FilterResponse {
|
||||
position: Position;
|
||||
data: Map<Id, Partial<Model>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all models of one collection that satisfy the filter condition.
|
||||
* Url: POST to /internal/datastore/reader/filter
|
||||
*
|
||||
* The global max position of the datastore is returned next the the filtered data.
|
||||
* This method does not take a position and can not be used when browsing the history.
|
||||
*
|
||||
* @returns see get_many
|
||||
* @throws InvalidFormat
|
||||
*/
|
||||
filter(collection: Collection, filter: Filter, mapped_fields?: Field[]): FilterResponse
|
||||
|
||||
/**
|
||||
* Url: POST to /internal/datastore/reader/exists
|
||||
*
|
||||
* See `filter`, returns true, if at least one model was found. The returned position is
|
||||
* the highest position in the complete datastore.
|
||||
*
|
||||
* @throws InvalidFormat
|
||||
*/
|
||||
exists(collection: Collection, filter: Filter): {exists: boolean; position: Position;}
|
||||
|
||||
/**
|
||||
* Url: POST to /internal/datastore/reader/count
|
||||
*
|
||||
* See `filter`, returns the amount of found models. The returned position is
|
||||
* the highest position in the complete datastore.
|
||||
*
|
||||
* @throws InvalidFormat
|
||||
*/
|
||||
count(collection: Collection, filter: Filter): {count: number; position: Position;}
|
||||
|
||||
/**
|
||||
* Url: POST to /internal/datastore/reader/min
|
||||
*
|
||||
* Executes a min aggregation on all models of one collection on
|
||||
* the given field that satisfy the filter condition.
|
||||
* The field is cast to int by default. If aggregation of another field type is needed,
|
||||
* a valid type can be passed via the type parameter.
|
||||
*
|
||||
* @throws InvalidFormat
|
||||
*/
|
||||
min(collection: Collection, filter: Filter, field: Field, type?: string): {min: Value; position: Position;}
|
||||
|
||||
/**
|
||||
* Url: POST to /internal/datastore/reader/max
|
||||
* Analogous to min.
|
||||
*
|
||||
* @throws InvalidFormat
|
||||
*/
|
||||
max(collection: Collection, filter: Filter, field: Field, type?: string): {max: Value; position: Position;}
|
||||
|
||||
Type Filter = And | Or | Not | FilterOperator
|
||||
|
||||
/**
|
||||
* The filter predicate. M[field] states the value of the field of a model M.
|
||||
* For all operations the predicate if true, if `M[field] <op> value` is true.
|
||||
*/
|
||||
Interface FilterOperator {
|
||||
field: Field;
|
||||
value: Value | null;
|
||||
operator: '=' | '!=' | '<' | '>' | '>=' | '<=';
|
||||
}
|
||||
|
||||
Interface Not {
|
||||
not_filter: Filter;
|
||||
}
|
||||
|
||||
Interface And {
|
||||
and_filter: Filter[];
|
||||
}
|
||||
|
||||
Interface Or {
|
||||
or_filter: Filter[];
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
## How to specify an interface for a service?
|
||||
|
||||
There are many ways to describe, what a service should do. The main
|
||||
characteristics (from an outside-view) are the provided functions. Each service
|
||||
does have functions to execute with arguments and return types. Next to the
|
||||
returnvalue, events can be published or errors be thrown. The way we want to
|
||||
derscribe these interfaces is through a TypeScript-like interface definition.
|
||||
The interface is the collection of all functions defined.
|
||||
|
||||
Example:
|
||||
/**
|
||||
* The Exception, if a model wasn't found. The missed id is given.
|
||||
*/
|
||||
Exception ObjectNotFound(id: Id);
|
||||
|
||||
/**
|
||||
* Does something..
|
||||
*
|
||||
* @throws ObjectNotFound
|
||||
do_it(data: MyRequest): void publishes MyEvent;
|
||||
|
||||
Interface MyRequest {
|
||||
id: Id;
|
||||
text: string;
|
||||
some_other_value: number;
|
||||
}
|
||||
|
||||
Event MyEvent on topic MyEventTopic {
|
||||
changed_text: string;
|
||||
}
|
||||
|
||||
There are some common types defined here:
|
||||
Type Position=number;
|
||||
Type Id=number;
|
||||
Type Fqid=string; // `collection/id`
|
||||
Type Fqfield=string; // `collection/id/field`
|
||||
Type Collection=string; // `collection`
|
||||
Type CollectionField=string; // `collection/field`
|
||||
Type Model=object;
|
||||
Type Field=string;
|
||||
Type Value=any;
|
||||
|
||||
The language is a bit modified:
|
||||
|
||||
1) Exceptions
|
||||
Exceptions should be declared by giving the name and parameters:
|
||||
# Exception <name>(<param1>: <type1>, ...);
|
||||
A comment should state, why this exception can be thrown
|
||||
|
||||
2) Events in the function declaration
|
||||
Each function takes some arguments and have a return type. Next to the
|
||||
return type, some events can be published. With the `publishes` token, one
|
||||
can give a comma seperated list of events, that can be generated.
|
||||
# my_function(): void publishes Event1, Event2
|
||||
|
||||
Events should be specified as interfaces, but named `Events`. Also there is
|
||||
the `on topic` token, on which topic the message is broadcasted.
|
||||
# Event MyEvent on topic MyEventTopic { ... }
|
||||
|
||||
3) Functiondocumentation
|
||||
Each function must have a description. Also it should tell, what exceptions
|
||||
could be thrown.
|
||||
|
||||
4) Placeholders
|
||||
If you need generic names, use placeholders: `<my_placeholder>`
|
||||
E.g. if the event topic is specific for one id, the topic used may be
|
||||
definied as this example:
|
||||
# Event MyEvent on topic MyEventForUser<user_id> { ... }
|
@ -1,35 +0,0 @@
|
||||
/**
|
||||
* Saves a mediafile (encoded as base64) into this service.
|
||||
*
|
||||
* Technical:
|
||||
* POST to /internal/media/upload_mediafile/ with JSON payload.
|
||||
* Returns 200 on success, 4xx on errors with a message in the payload.
|
||||
*/
|
||||
upload_mediafile(file: string, id: Id, mimetype: string): {message: string} | void
|
||||
|
||||
/**
|
||||
* Saves a resource (encoded as base64) into this service.
|
||||
*
|
||||
* Technical:
|
||||
* POST to /internal/media/upload_resource/ with JSON payload.
|
||||
* Returns 200 on success, 4xx on errors with a message in the payload.
|
||||
*/
|
||||
upload_resource(file: string, id: Id, mimetype: string): {message: string} | void
|
||||
|
||||
/**
|
||||
* Retrieves a mediafile given by it's id.
|
||||
*
|
||||
* Technical:
|
||||
* GET to /system/media/get/<mediafile_id>
|
||||
* Returns 200 on success and 404, if the resource does not exist
|
||||
*/
|
||||
get(mediafile_id: Id): Blob
|
||||
|
||||
/**
|
||||
* Retrieves a resource given by it's id.
|
||||
*
|
||||
* Technical:
|
||||
* GET to /system/media/get_resource/<resource_id>
|
||||
* Returns 200 on success and 404, if the resource does not exist
|
||||
*/
|
||||
get(resource_id: Id): Blob
|
@ -1,53 +0,0 @@
|
||||
# Permission Service Interface
|
||||
|
||||
|
||||
/**
|
||||
* Returns true, if the thing requested (identified by `name`) is allowed for each
|
||||
* provided data in `dataList` by the user given by `user_id`.
|
||||
*
|
||||
* Convention: For each action and presenter, there will be an permission check.
|
||||
* This is not enforced but a good guideline to see which permission check belongs
|
||||
* to which usecase.
|
||||
**/
|
||||
is_allowed(name: string, user_id: Id, dataList: object[]): bool
|
||||
|
||||
|
||||
/**
|
||||
* Filters the fqfields that can ve deen by the user. Only fqfields are returned, that are accessible.
|
||||
**/
|
||||
restrict_fqfields(fqfields: Fqfield[], user_id: Id): Fqfield[]
|
||||
|
||||
|
||||
/**
|
||||
* Should be called for each update of the datastore with the changed data. It
|
||||
* returns user ids, which should get a full update since too many/complicated
|
||||
* permission changes accur.
|
||||
*
|
||||
* Possible reasons for additional updates:
|
||||
* 1) A permission related relation to a user has changed:
|
||||
* - Relation to a group
|
||||
* - Relation to a meeting (via guest/temporary relation)
|
||||
* - Relation to a committee (or an upgrade/downgrade as a manager)
|
||||
* 2) Role of a user has changed
|
||||
* 3) Permissions of a group changed -> Full update for all users in this group
|
||||
* 4) Changes in specific (meeting-related) models:
|
||||
* - Motion submitter:
|
||||
* - Motion state: Update des Antrages
|
||||
* - Motion block internal: Update des Blocks
|
||||
* - Motion comment section read groups: Update aller Comments dieser Section
|
||||
* - State restrictions: Update aller Anträge in dem State
|
||||
* - Agendaitem visibility: Update des Agendaitems
|
||||
* - Poll state: Wenn state==published volles update aller options/votes
|
||||
* - Mediafile (has_)inherited_access_groups: Update der Mediafile
|
||||
* - Mediafile used_as_*: Update der Mediafile
|
||||
*
|
||||
**/
|
||||
additional_update(updated: {[fqfield: Fqfield]: Value}): Id[]
|
||||
|
||||
/**
|
||||
* This technical interface must be implemented by the services
|
||||
* users.
|
||||
*/
|
||||
Interface DataProvider {
|
||||
get: (fqfields: Fqfield[]) => {[fqfield: Fqfield]: Value}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
# Presenter Service Interface
|
||||
|
||||
Exception PresenterException(message: string);
|
||||
|
||||
/**
|
||||
* Executes some presenting function on the server. The term "presenting" means
|
||||
* a non writing (or modifying) idempotent request. There may be some
|
||||
* side-effect allowed (like tracking calls to one presenter), but the main
|
||||
* purpose is to get data of the server, which is not autoupdate-, projector-, or
|
||||
* icc-data.
|
||||
*
|
||||
* Some purposes may be:
|
||||
* - aggregating of login data
|
||||
* - managing of whoami, if additional data is needed, that the auth service
|
||||
* doesn't provide
|
||||
* - To get history points
|
||||
* - To get history data
|
||||
* - To get the installed version
|
||||
* - To query statistics
|
||||
* - To calculate recursive trees (e.g. origins of motions; not possible with
|
||||
* the autoupdate service)
|
||||
* - to synchronize the servertime for countdowns
|
||||
*
|
||||
* @throws PresenterException This exception might be thrown, if there was an
|
||||
* server error (Http-500-equivalent). For user error (e.g. wrong data) use the
|
||||
* PresenterError interface.
|
||||
*/
|
||||
handle_request(payload: Presenter[], user_id: Id): PresenterResult[]
|
||||
|
||||
/**
|
||||
* This interface specifies what presenter is used with the payload for the call.
|
||||
*/
|
||||
Interface Presenter {
|
||||
presenter: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* A presenter may return anything. This is presenter-specific. But if there was
|
||||
* an error (user error, not a PresenterException), the response for this
|
||||
* presenter should follow `PresenterError`.
|
||||
*
|
||||
* TODO: Maybe we do not want to response an error but always throw an exception
|
||||
* instead.
|
||||
*/
|
||||
type PresenterResult = any | PresenterError;
|
||||
|
||||
/**
|
||||
* A common format for errors.
|
||||
*/
|
||||
Interface PresenterError {
|
||||
error: object;
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
# Restrictions Service Interface
|
||||
|
||||
Exception RestrictionException(message: string);
|
||||
|
||||
|
||||
/**
|
||||
* Restricts data for given user
|
||||
*
|
||||
* @throws RestrictionException
|
||||
*/
|
||||
handle_request (payload: Restriction[]): RestrictionResult[]
|
||||
|
||||
interface Restriction {
|
||||
user_id: number;
|
||||
fqfields: Fqfield[];
|
||||
}
|
||||
|
||||
interface RestrictionResult {
|
||||
<fqfield>: Value;
|
||||
}
|
@ -1 +0,0 @@
|
||||
<mxfile host="app.diagrams.net" modified="2020-12-04T15:16:43.854Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36" etag="w7B7nepetiGU9l5b0a2N" version="13.10.8" type="device"><diagram id="SzigwTKClLag4ZUmXMWD" name="Page-1">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</diagram></mxfile>
|
@ -1 +1 @@
|
||||
Subproject commit fa36ec3b33af85367ffdf769c43c296c1afca9cc
|
||||
Subproject commit 3897450e038fb4ef6ed6e72f32d4ca613b31b347
|
@ -1 +1 @@
|
||||
Subproject commit 2175dc2ccc277c585891551acb22b7fcbccc0d40
|
||||
Subproject commit fdbb91580a8cd31ac23230b2f2a9fd5354599876
|
@ -1 +1 @@
|
||||
Subproject commit bc083f8c07e8952759f38831f636890a72a08f34
|
||||
Subproject commit 0bc9c3ef3fad0488ad4d914030ab50a2c3915f28
|
@ -1 +1 @@
|
||||
Subproject commit daa548c253e761f65a426ca02d6c08a62a5bd221
|
||||
Subproject commit 3911fee5c58eda349dc574d93f2f782e6c95ce91
|
Loading…
Reference in New Issue
Block a user