Merge branch 'bentiss-openapi' into devel

This commit is contained in:
Lauri Ojansivu 2019-01-19 21:09:42 +02:00
commit 3257f78d24
15 changed files with 1971 additions and 44 deletions

View file

@ -75,7 +75,7 @@ ARG DEFAULT_AUTHENTICATION_METHOD
# Set the environment variables (defaults where required)
# DOES NOT WORK: paxctl fix for alpine linux: https://github.com/wekan/wekan/issues/1303
# ENV BUILD_DEPS="paxctl"
ENV BUILD_DEPS="apt-utils bsdtar gnupg gosu wget curl bzip2 build-essential python git ca-certificates gcc-7" \
ENV BUILD_DEPS="apt-utils bsdtar gnupg gosu wget curl bzip2 build-essential python python3 python3-distutils git ca-certificates gcc-7" \
NODE_VERSION=v8.15.0 \
METEOR_RELEASE=1.6.0.1 \
USE_EDGE=false \
@ -251,6 +251,18 @@ RUN \
cd /home/wekan/.meteor && \
gosu wekan:wekan /home/wekan/.meteor/meteor -- help; \
\
# extract the OpenAPI specification
npm install -g api2html && \
mkdir -p /home/wekan/python && \
chown wekan:wekan --recursive /home/wekan/python && \
cd /home/wekan/python && \
gosu wekan:wekan git clone --depth 1 -b master git://github.com/Kronuz/esprima-python && \
cd /home/wekan/python/esprima-python && \
python3 setup.py install --record files.txt && \
cd /home/wekan/app &&\
mkdir -p ./public/api && \
python3 ./openapi/generate_openapi.py --release $(git describe --tags --abbrev=0) > ./public/api/wekan.yml && \
/opt/nodejs/bin/api2html -c ./public/wekan-logo-header.png -o ./public/api/wekan.html ./public/api/wekan.yml; \
# Build app
cd /home/wekan/app && \
gosu wekan:wekan /home/wekan/.meteor/meteor add standard-minifier-js && \
@ -275,10 +287,13 @@ RUN \
# Cleanup
apt-get remove --purge -y ${BUILD_DEPS} && \
apt-get autoremove -y && \
npm uninstall -g api2html &&\
rm -R /var/lib/apt/lists/* && \
rm -R /home/wekan/.meteor && \
rm -R /home/wekan/app && \
rm -R /home/wekan/app_build && \
cat /home/wekan/python/esprima-python/files.txt | xargs rm -R && \
rm -R /home/wekan/python && \
rm /home/wekan/install_meteor.sh
ENV PORT=8080

View file

@ -1,10 +1,19 @@
Boards = new Mongo.Collection('boards');
/**
* This is a Board.
*/
Boards.attachSchema(new SimpleSchema({
title: {
/**
* The title of the board
*/
type: String,
},
slug: {
/**
* The title slugified.
*/
type: String,
autoValue() { // eslint-disable-line consistent-return
// XXX We need to improve slug management. Only the id should be necessary
@ -24,6 +33,9 @@ Boards.attachSchema(new SimpleSchema({
},
},
archived: {
/**
* Is the board archived?
*/
type: Boolean,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert && !this.isSet) {
@ -32,6 +44,9 @@ Boards.attachSchema(new SimpleSchema({
},
},
createdAt: {
/**
* Creation time of the board
*/
type: Date,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert) {
@ -43,6 +58,9 @@ Boards.attachSchema(new SimpleSchema({
},
// XXX Inconsistent field naming
modifiedAt: {
/**
* Last modification time of the board
*/
type: Date,
optional: true,
autoValue() { // eslint-disable-line consistent-return
@ -55,6 +73,9 @@ Boards.attachSchema(new SimpleSchema({
},
// De-normalized number of users that have starred this board
stars: {
/**
* How many stars the board has
*/
type: Number,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert) {
@ -64,6 +85,9 @@ Boards.attachSchema(new SimpleSchema({
},
// De-normalized label system
'labels': {
/**
* List of labels attached to a board
*/
type: [Object],
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert && !this.isSet) {
@ -78,6 +102,9 @@ Boards.attachSchema(new SimpleSchema({
},
},
'labels.$._id': {
/**
* Unique id of a label
*/
// We don't specify that this field must be unique in the board because that
// will cause performance penalties and is not necessary since this field is
// always set on the server.
@ -86,10 +113,22 @@ Boards.attachSchema(new SimpleSchema({
type: String,
},
'labels.$.name': {
/**
* Name of a label
*/
type: String,
optional: true,
},
'labels.$.color': {
/**
* color of a label.
*
* Can be amongst `green`, `yellow`, `orange`, `red`, `purple`,
* `blue`, `sky`, `lime`, `pink`, `black`,
* `silver`, `peachpuff`, `crimson`, `plum`, `darkgreen`,
* `slateblue`, `magenta`, `gold`, `navy`, `gray`,
* `saddlebrown`, `paleturquoise`, `mistyrose`, `indigo`
*/
type: String,
allowedValues: [
'green', 'yellow', 'orange', 'red', 'purple',
@ -103,6 +142,9 @@ Boards.attachSchema(new SimpleSchema({
// documents like de-normalized meta-data (the date the member joined the
// board, the number of contributions, etc.).
'members': {
/**
* List of members of a board
*/
type: [Object],
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert && !this.isSet) {
@ -117,27 +159,48 @@ Boards.attachSchema(new SimpleSchema({
},
},
'members.$.userId': {
/**
* The uniq ID of the member
*/
type: String,
},
'members.$.isAdmin': {
/**
* Is the member an admin of the board?
*/
type: Boolean,
},
'members.$.isActive': {
/**
* Is the member active?
*/
type: Boolean,
},
'members.$.isNoComments': {
/**
* Is the member not allowed to make comments
*/
type: Boolean,
optional: true,
},
'members.$.isCommentOnly': {
/**
* Is the member only allowed to comment on the board
*/
type: Boolean,
optional: true,
},
permission: {
/**
* visibility of the board
*/
type: String,
allowedValues: ['public', 'private'],
},
color: {
/**
* The color of the board.
*/
type: String,
allowedValues: [
'belize',
@ -154,24 +217,45 @@ Boards.attachSchema(new SimpleSchema({
},
},
description: {
/**
* The description of the board
*/
type: String,
optional: true,
},
subtasksDefaultBoardId: {
/**
* The default board ID assigned to subtasks.
*/
type: String,
optional: true,
defaultValue: null,
},
subtasksDefaultListId: {
/**
* The default List ID assigned to subtasks.
*/
type: String,
optional: true,
defaultValue: null,
},
allowsSubtasks: {
/**
* Does the board allows subtasks?
*/
type: Boolean,
defaultValue: true,
},
presentParentTask: {
/**
* Controls how to present the parent task:
*
* - `prefix-with-full-path`: add a prefix with the full path
* - `prefix-with-parent`: add a prefisx with the parent name
* - `subtext-with-full-path`: add a subtext with the full path
* - `subtext-with-parent`: add a subtext with the parent name
* - `no-parent`: does not show the parent at all
*/
type: String,
allowedValues: [
'prefix-with-full-path',
@ -184,23 +268,38 @@ Boards.attachSchema(new SimpleSchema({
defaultValue: 'no-parent',
},
startAt: {
/**
* Starting date of the board.
*/
type: Date,
optional: true,
},
dueAt: {
/**
* Due date of the board.
*/
type: Date,
optional: true,
},
endAt: {
/**
* End date of the board.
*/
type: Date,
optional: true,
},
spentTime: {
/**
* Time spent in the board.
*/
type: Number,
decimal: true,
optional: true,
},
isOvertime: {
/**
* Is the board overtimed?
*/
type: Boolean,
defaultValue: false,
optional: true,
@ -278,10 +377,6 @@ Boards.helpers({
return Users.find({ _id: { $in: _.pluck(this.members, 'userId') } });
},
getMember(id) {
return _.findWhere(this.members, { userId: id });
},
getLabel(name, color) {
return _.findWhere(this.labels, { name, color });
},
@ -778,6 +873,14 @@ if (Meteor.isServer) {
//BOARDS REST API
if (Meteor.isServer) {
/**
* @operation get_boards_from_user
* @summary Get all boards attached to a user
*
* @param {string} userId the ID of the user to retrieve the data
* @return_type [{_id: string,
title: string}]
*/
JsonRoutes.add('GET', '/api/users/:userId/boards', function (req, res) {
try {
Authentication.checkLoggedIn(req.userId);
@ -808,6 +911,13 @@ if (Meteor.isServer) {
}
});
/**
* @operation get_public_boards
* @summary Get all public boards
*
* @return_type [{_id: string,
title: string}]
*/
JsonRoutes.add('GET', '/api/boards', function (req, res) {
try {
Authentication.checkUserId(req.userId);
@ -829,6 +939,13 @@ if (Meteor.isServer) {
}
});
/**
* @operation get_board
* @summary Get the board with that particular ID
*
* @param {string} boardId the ID of the board to retrieve the data
* @return_type Boards
*/
JsonRoutes.add('GET', '/api/boards/:boardId', function (req, res) {
try {
const id = req.params.boardId;
@ -847,34 +964,31 @@ if (Meteor.isServer) {
}
});
JsonRoutes.add('PUT', '/api/boards/:boardId/members', function (req, res) {
Authentication.checkUserId(req.userId);
try {
const boardId = req.params.boardId;
const board = Boards.findOne({ _id: boardId });
const userId = req.body.userId;
const user = Users.findOne({ _id: userId });
if (!board.getMember(userId)) {
user.addInvite(boardId);
board.addMember(userId);
JsonRoutes.sendResult(res, {
code: 200,
data: id,
});
} else {
JsonRoutes.sendResult(res, {
code: 200,
});
}
}
catch (error) {
JsonRoutes.sendResult(res, {
data: error,
});
}
});
/**
* @operation new_board
* @summary Create a board
*
* @description This allows to create a board.
*
* The color has to be chosen between `belize`, `nephritis`, `pomegranate`,
* `pumpkin`, `wisteria`, `midnight`:
*
* <img src="https://wekan.github.io/board-colors.png" width="40%" alt="Wekan logo" />
*
* @param {string} title the new title of the board
* @param {string} owner "ABCDE12345" <= User ID in Wekan.
* (Not username or email)
* @param {boolean} [isAdmin] is the owner an admin of the board (default true)
* @param {boolean} [isActive] is the board active (default true)
* @param {boolean} [isNoComments] disable comments (default false)
* @param {boolean} [isCommentOnly] only enable comments (default false)
* @param {string} [permission] "private" board <== Set to "public" if you
* want public Wekan board
* @param {string} [color] the color of the board
*
* @return_type {_id: string,
defaultSwimlaneId: string}
*/
JsonRoutes.add('POST', '/api/boards', function (req, res) {
try {
Authentication.checkUserId(req.userId);
@ -912,6 +1026,12 @@ if (Meteor.isServer) {
}
});
/**
* @operation delete_board
* @summary Delete a board
*
* @param {string} boardId the ID of the board
*/
JsonRoutes.add('DELETE', '/api/boards/:boardId', function (req, res) {
try {
Authentication.checkUserId(req.userId);
@ -932,6 +1052,19 @@ if (Meteor.isServer) {
}
});
/**
* @operation add_board_label
* @summary Add a label to a board
*
* @description If the board doesn't have the name/color label, this function
* adds the label to the board.
*
* @param {string} boardId the board
* @param {string} color the color of the new label
* @param {string} name the name of the new label
*
* @return_type string
*/
JsonRoutes.add('PUT', '/api/boards/:boardId/labels', function (req, res) {
Authentication.checkUserId(req.userId);
const id = req.params.boardId;
@ -961,6 +1094,17 @@ if (Meteor.isServer) {
}
});
/**
* @operation set_board_member_permission
* @tag Users
* @summary Change the permission of a member of a board
*
* @param {string} boardId the ID of the board that we are changing
* @param {string} memberId the ID of the user to change permissions
* @param {boolean} isAdmin admin capability
* @param {boolean} isNoComments NoComments capability
* @param {boolean} isCommentOnly CommentsOnly capability
*/
JsonRoutes.add('POST', '/api/boards/:boardId/members/:memberId', function (req, res) {
try {
const boardId = req.params.boardId;

View file

@ -1,19 +1,34 @@
CardComments = new Mongo.Collection('card_comments');
/**
* A comment on a card
*/
CardComments.attachSchema(new SimpleSchema({
boardId: {
/**
* the board ID
*/
type: String,
},
cardId: {
/**
* the card ID
*/
type: String,
},
// XXX Rename in `content`? `text` is a bit vague...
text: {
/**
* the text of the comment
*/
type: String,
},
// XXX We probably don't need this information here, since we already have it
// in the associated comment creation activity
createdAt: {
/**
* when was the comment created
*/
type: Date,
denyUpdate: false,
autoValue() { // eslint-disable-line consistent-return
@ -26,6 +41,9 @@ CardComments.attachSchema(new SimpleSchema({
},
// XXX Should probably be called `authorId`
userId: {
/**
* the author ID of the comment
*/
type: String,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert && !this.isSet) {
@ -87,6 +105,16 @@ if (Meteor.isServer) {
//CARD COMMENT REST API
if (Meteor.isServer) {
/**
* @operation get_all_comments
* @summary Get all comments attached to a card
*
* @param {string} boardId the board ID of the card
* @param {string} cardId the ID of the card
* @return_type [{_id: string,
* comment: string,
* authorId: string}]
*/
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/comments', function (req, res) {
try {
Authentication.checkUserId( req.userId);
@ -111,6 +139,15 @@ if (Meteor.isServer) {
}
});
/**
* @operation get_comment
* @summary Get a comment on a card
*
* @param {string} boardId the board ID of the card
* @param {string} cardId the ID of the card
* @param {string} commentId the ID of the comment to retrieve
* @return_type CardComments
*/
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/comments/:commentId', function (req, res) {
try {
Authentication.checkUserId( req.userId);
@ -130,6 +167,16 @@ if (Meteor.isServer) {
}
});
/**
* @operation new_comment
* @summary Add a comment on a card
*
* @param {string} boardId the board ID of the card
* @param {string} cardId the ID of the card
* @param {string} authorId the user who 'posted' the comment
* @param {string} text the content of the comment
* @return_type {_id: string}
*/
JsonRoutes.add('POST', '/api/boards/:boardId/cards/:cardId/comments', function (req, res) {
try {
Authentication.checkUserId( req.userId);
@ -160,6 +207,15 @@ if (Meteor.isServer) {
}
});
/**
* @operation delete_comment
* @summary Delete a comment on a card
*
* @param {string} boardId the board ID of the card
* @param {string} cardId the ID of the card
* @param {string} commentId the ID of the comment to delete
* @return_type {_id: string}
*/
JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/comments/:commentId', function (req, res) {
try {
Authentication.checkUserId( req.userId);

View file

@ -5,11 +5,17 @@ Cards = new Mongo.Collection('cards');
// of comments just to display the number of them in the board view.
Cards.attachSchema(new SimpleSchema({
title: {
/**
* the title of the card
*/
type: String,
optional: true,
defaultValue: '',
},
archived: {
/**
* is the card archived
*/
type: Boolean,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert && !this.isSet) {
@ -18,33 +24,51 @@ Cards.attachSchema(new SimpleSchema({
},
},
parentId: {
/**
* ID of the parent card
*/
type: String,
optional: true,
defaultValue: '',
},
listId: {
/**
* List ID where the card is
*/
type: String,
optional: true,
defaultValue: '',
},
swimlaneId: {
/**
* Swimlane ID where the card is
*/
type: String,
},
// The system could work without this `boardId` information (we could deduce
// the board identifier from the card), but it would make the system more
// difficult to manage and less efficient.
boardId: {
/**
* Board ID of the card
*/
type: String,
optional: true,
defaultValue: '',
},
coverId: {
/**
* Cover ID of the card
*/
type: String,
optional: true,
defaultValue: '',
},
createdAt: {
/**
* creation date
*/
type: Date,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert) {
@ -55,6 +79,9 @@ Cards.attachSchema(new SimpleSchema({
},
},
customFields: {
/**
* list of custom fields
*/
type: [Object],
optional: true,
defaultValue: [],
@ -62,11 +89,17 @@ Cards.attachSchema(new SimpleSchema({
'customFields.$': {
type: new SimpleSchema({
_id: {
/**
* the ID of the related custom field
*/
type: String,
optional: true,
defaultValue: '',
},
value: {
/**
* value attached to the custom field
*/
type: Match.OneOf(String, Number, Boolean, Date),
optional: true,
defaultValue: '',
@ -74,59 +107,95 @@ Cards.attachSchema(new SimpleSchema({
}),
},
dateLastActivity: {
/**
* Date of last activity
*/
type: Date,
autoValue() {
return new Date();
},
},
description: {
/**
* description of the card
*/
type: String,
optional: true,
defaultValue: '',
},
requestedBy: {
/**
* who requested the card (ID of the user)
*/
type: String,
optional: true,
defaultValue: '',
},
assignedBy: {
/**
* who assigned the card (ID of the user)
*/
type: String,
optional: true,
defaultValue: '',
},
labelIds: {
/**
* list of labels ID the card has
*/
type: [String],
optional: true,
defaultValue: [],
},
members: {
/**
* list of members (user IDs)
*/
type: [String],
optional: true,
defaultValue: [],
},
receivedAt: {
/**
* Date the card was received
*/
type: Date,
optional: true,
},
startAt: {
/**
* Date the card was started to be worked on
*/
type: Date,
optional: true,
},
dueAt: {
/**
* Date the card is due
*/
type: Date,
optional: true,
},
endAt: {
/**
* Date the card ended
*/
type: Date,
optional: true,
},
spentTime: {
/**
* How much time has been spent on this
*/
type: Number,
decimal: true,
optional: true,
defaultValue: 0,
},
isOvertime: {
/**
* is the card over time?
*/
type: Boolean,
defaultValue: false,
optional: true,
@ -134,6 +203,9 @@ Cards.attachSchema(new SimpleSchema({
// XXX Should probably be called `authorId`. Is it even needed since we have
// the `members` field?
userId: {
/**
* user ID of the author of the card
*/
type: String,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert && !this.isSet) {
@ -142,21 +214,33 @@ Cards.attachSchema(new SimpleSchema({
},
},
sort: {
/**
* Sort value
*/
type: Number,
decimal: true,
defaultValue: '',
},
subtaskSort: {
/**
* subtask sort value
*/
type: Number,
decimal: true,
defaultValue: -1,
optional: true,
},
type: {
/**
* type of the card
*/
type: String,
defaultValue: '',
},
linkedId: {
/**
* ID of the linked card
*/
type: String,
optional: true,
defaultValue: '',
@ -1309,6 +1393,17 @@ if (Meteor.isServer) {
}
//SWIMLANES REST API
if (Meteor.isServer) {
/**
* @operation get_swimlane_cards
* @summary get all cards attached to a swimlane
*
* @param {string} boardId the board ID
* @param {string} swimlaneId the swimlane ID
* @return_type [{_id: string,
* title: string,
* description: string,
* listId: string}]
*/
JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes/:swimlaneId/cards', function(req, res) {
const paramBoardId = req.params.boardId;
const paramSwimlaneId = req.params.swimlaneId;
@ -1332,6 +1427,16 @@ if (Meteor.isServer) {
}
//LISTS REST API
if (Meteor.isServer) {
/**
* @operation get_all_cards
* @summary get all cards attached to a list
*
* @param {string} boardId the board ID
* @param {string} listId the list ID
* @return_type [{_id: string,
* title: string,
* description: string}]
*/
JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId/cards', function(req, res) {
const paramBoardId = req.params.boardId;
const paramListId = req.params.listId;
@ -1352,6 +1457,15 @@ if (Meteor.isServer) {
});
});
/**
* @operation get_card
* @summary get a card
*
* @param {string} boardId the board ID
* @param {string} listId the list ID of the card
* @param {string} cardId the card ID
* @return_type Cards
*/
JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId/cards/:cardId', function(req, res) {
const paramBoardId = req.params.boardId;
const paramListId = req.params.listId;
@ -1368,6 +1482,19 @@ if (Meteor.isServer) {
});
});
/**
* @operation new_card
* @summary creates a new card
*
* @param {string} boardId the board ID of the new card
* @param {string} listId the list ID of the new card
* @param {string} authorID the user ID of the person owning the card
* @param {string} title the title of the new card
* @param {string} description the description of the new card
* @param {string} swimlaneId the swimlane ID of the new card
* @param {string} [members] the member IDs list of the new card
* @return_type {_id: string}
*/
JsonRoutes.add('POST', '/api/boards/:boardId/lists/:listId/cards', function(req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
@ -1406,6 +1533,36 @@ if (Meteor.isServer) {
}
});
/*
* Note for the JSDoc:
* 'list' will be interpreted as the path parameter
* 'listID' will be interpreted as the body parameter
*/
/**
* @operation edit_card
* @summary edit fields in a card
*
* @param {string} boardId the board ID of the card
* @param {string} list the list ID of the card
* @param {string} cardId the ID of the card
* @param {string} [title] the new title of the card
* @param {string} [listId] the new list ID of the card (move operation)
* @param {string} [description] the new description of the card
* @param {string} [authorId] change the owner of the card
* @param {string} [labelIds] the new list of label IDs attached to the card
* @param {string} [swimlaneId] the new swimlane ID of the card
* @param {string} [members] the new list of member IDs attached to the card
* @param {string} [requestedBy] the new requestedBy field of the card
* @param {string} [assignedBy] the new assignedBy field of the card
* @param {string} [receivedAt] the new receivedAt field of the card
* @param {string} [assignBy] the new assignBy field of the card
* @param {string} [startAt] the new startAt field of the card
* @param {string} [dueAt] the new dueAt field of the card
* @param {string} [endAt] the new endAt field of the card
* @param {string} [spentTime] the new spentTime field of the card
* @param {boolean} [isOverTime] the new isOverTime field of the card
* @param {string} [customFields] the new customFields value of the card
*/
JsonRoutes.add('PUT', '/api/boards/:boardId/lists/:listId/cards/:cardId', function(req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
@ -1551,6 +1708,18 @@ if (Meteor.isServer) {
});
});
/**
* @operation delete_card
* @summary Delete a card from a board
*
* @description This operation **deletes** a card, and therefore the card
* is not put in the recycle bin.
*
* @param {string} boardId the board ID of the card
* @param {string} list the list ID of the card
* @param {string} cardId the ID of the card
* @return_type {_id: string}
*/
JsonRoutes.add('DELETE', '/api/boards/:boardId/lists/:listId/cards/:cardId', function(req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;

View file

@ -1,21 +1,39 @@
ChecklistItems = new Mongo.Collection('checklistItems');
/**
* An item in a checklist
*/
ChecklistItems.attachSchema(new SimpleSchema({
title: {
/**
* the text of the item
*/
type: String,
},
sort: {
/**
* the sorting field of the item
*/
type: Number,
decimal: true,
},
isFinished: {
/**
* Is the item checked?
*/
type: Boolean,
defaultValue: false,
},
checklistId: {
/**
* the checklist ID the item is attached to
*/
type: String,
},
cardId: {
/**
* the card ID the item is attached to
*/
type: String,
},
}));
@ -193,6 +211,17 @@ if (Meteor.isServer) {
}
if (Meteor.isServer) {
/**
* @operation get_checklist_item
* @tag Checklists
* @summary Get a checklist item
*
* @param {string} boardId the board ID
* @param {string} cardId the card ID
* @param {string} checklistId the checklist ID
* @param {string} itemId the ID of the item
* @return_type ChecklistItems
*/
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', function (req, res) {
Authentication.checkUserId( req.userId);
const paramItemId = req.params.itemId;
@ -209,6 +238,19 @@ if (Meteor.isServer) {
}
});
/**
* @operation edit_checklist_item
* @tag Checklists
* @summary Edit a checklist item
*
* @param {string} boardId the board ID
* @param {string} cardId the card ID
* @param {string} checklistId the checklist ID
* @param {string} itemId the ID of the item
* @param {string} [isFinished] is the item checked?
* @param {string} [title] the new text of the item
* @return_type {_id: string}
*/
JsonRoutes.add('PUT', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', function (req, res) {
Authentication.checkUserId( req.userId);
@ -229,6 +271,19 @@ if (Meteor.isServer) {
});
});
/**
* @operation delete_checklist_item
* @tag Checklists
* @summary Delete a checklist item
*
* @description Note: this operation can't be reverted.
*
* @param {string} boardId the board ID
* @param {string} cardId the card ID
* @param {string} checklistId the checklist ID
* @param {string} itemId the ID of the item to be removed
* @return_type {_id: string}
*/
JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', function (req, res) {
Authentication.checkUserId( req.userId);
const paramItemId = req.params.itemId;

View file

@ -1,18 +1,33 @@
Checklists = new Mongo.Collection('checklists');
/**
* A Checklist
*/
Checklists.attachSchema(new SimpleSchema({
cardId: {
/**
* The ID of the card the checklist is in
*/
type: String,
},
title: {
/**
* the title of the checklist
*/
type: String,
defaultValue: 'Checklist',
},
finishedAt: {
/**
* When was the checklist finished
*/
type: Date,
optional: true,
},
createdAt: {
/**
* Creation date of the checklist
*/
type: Date,
denyUpdate: false,
autoValue() { // eslint-disable-line consistent-return
@ -24,6 +39,9 @@ Checklists.attachSchema(new SimpleSchema({
},
},
sort: {
/**
* sorting value of the checklist
*/
type: Number,
decimal: true,
},
@ -128,6 +146,15 @@ if (Meteor.isServer) {
}
if (Meteor.isServer) {
/**
* @operation get_all_checklists
* @summary Get the list of checklists attached to a card
*
* @param {string} boardId the board ID
* @param {string} cardId the card ID
* @return_type [{_id: string,
* title: string}]
*/
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res) {
Authentication.checkUserId( req.userId);
const paramCardId = req.params.cardId;
@ -149,6 +176,22 @@ if (Meteor.isServer) {
}
});
/**
* @operation get_checklist
* @summary Get a checklist
*
* @param {string} boardId the board ID
* @param {string} cardId the card ID
* @param {string} checklistId the ID of the checklist
* @return_type {cardId: string,
* title: string,
* finishedAt: string,
* createdAt: string,
* sort: number,
* items: [{_id: string,
* title: string,
* isFinished: boolean}]}
*/
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId', function (req, res) {
Authentication.checkUserId( req.userId);
const paramChecklistId = req.params.checklistId;
@ -173,6 +216,15 @@ if (Meteor.isServer) {
}
});
/**
* @operation new_checklist
* @summary create a new checklist
*
* @param {string} boardId the board ID
* @param {string} cardId the card ID
* @param {string} title the title of the new checklist
* @return_type {_id: string}
*/
JsonRoutes.add('POST', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res) {
Authentication.checkUserId( req.userId);
@ -204,6 +256,17 @@ if (Meteor.isServer) {
}
});
/**
* @operation delete_checklist
* @summary Delete a checklist
*
* @description The checklist will be removed, not put in the recycle bin.
*
* @param {string} boardId the board ID
* @param {string} cardId the card ID
* @param {string} checklistId the ID of the checklist to remove
* @return_type {_id: string}
*/
JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId', function (req, res) {
Authentication.checkUserId( req.userId);
const paramChecklistId = req.params.checklistId;

View file

@ -1,40 +1,73 @@
CustomFields = new Mongo.Collection('customFields');
/**
* A custom field on a card in the board
*/
CustomFields.attachSchema(new SimpleSchema({
boardId: {
/**
* the ID of the board
*/
type: String,
},
name: {
/**
* name of the custom field
*/
type: String,
},
type: {
/**
* type of the custom field
*/
type: String,
allowedValues: ['text', 'number', 'date', 'dropdown'],
},
settings: {
/**
* settings of the custom field
*/
type: Object,
},
'settings.dropdownItems': {
/**
* list of drop down items objects
*/
type: [Object],
optional: true,
},
'settings.dropdownItems.$': {
type: new SimpleSchema({
_id: {
/**
* ID of the drop down item
*/
type: String,
},
name: {
/**
* name of the drop down item
*/
type: String,
},
}),
},
showOnCard: {
/**
* should we show on the cards this custom field
*/
type: Boolean,
},
automaticallyOnCard: {
/**
* should the custom fields automatically be added on cards?
*/
type: Boolean,
},
showLabelOnMiniCard: {
/**
* should the label of the custom field be shown on minicards?
*/
type: Boolean,
},
}));
@ -88,6 +121,15 @@ if (Meteor.isServer) {
//CUSTOM FIELD REST API
if (Meteor.isServer) {
/**
* @operation get_all_custom_fields
* @summary Get the list of Custom Fields attached to a board
*
* @param {string} boardID the ID of the board
* @return_type [{_id: string,
* name: string,
* type: string}]
*/
JsonRoutes.add('GET', '/api/boards/:boardId/custom-fields', function (req, res) {
Authentication.checkUserId( req.userId);
const paramBoardId = req.params.boardId;
@ -103,6 +145,14 @@ if (Meteor.isServer) {
});
});
/**
* @operation get_custom_field
* @summary Get a Custom Fields attached to a board
*
* @param {string} boardID the ID of the board
* @param {string} customFieldId the ID of the custom field
* @return_type CustomFields
*/
JsonRoutes.add('GET', '/api/boards/:boardId/custom-fields/:customFieldId', function (req, res) {
Authentication.checkUserId( req.userId);
const paramBoardId = req.params.boardId;
@ -113,6 +163,19 @@ if (Meteor.isServer) {
});
});
/**
* @operation new_custom_field
* @summary Create a Custom Field
*
* @param {string} boardID the ID of the board
* @param {string} name the name of the custom field
* @param {string} type the type of the custom field
* @param {string} settings the settings object of the custom field
* @param {boolean} showOnCard should we show the custom field on cards?
* @param {boolean} automaticallyOnCard should the custom fields automatically be added on cards?
* @param {boolean} showLabelOnMiniCard should the label of the custom field be shown on minicards?
* @return_type {_id: string}
*/
JsonRoutes.add('POST', '/api/boards/:boardId/custom-fields', function (req, res) {
Authentication.checkUserId( req.userId);
const paramBoardId = req.params.boardId;
@ -137,6 +200,16 @@ if (Meteor.isServer) {
});
});
/**
* @operation delete_custom_field
* @summary Delete a Custom Fields attached to a board
*
* @description The Custom Field can't be retrieved after this operation
*
* @param {string} boardID the ID of the board
* @param {string} customFieldId the ID of the custom field
* @return_type {_id: string}
*/
JsonRoutes.add('DELETE', '/api/boards/:boardId/custom-fields/:customFieldId', function (req, res) {
Authentication.checkUserId( req.userId);
const paramBoardId = req.params.boardId;

View file

@ -6,13 +6,20 @@ if (Meteor.isServer) {
// `ApiRoutes.path('boards/export', boardId)``
// on the client instead of copy/pasting the route path manually between the
// client and the server.
/*
* This route is used to export the board FROM THE APPLICATION.
* If user is already logged-in, pass loginToken as param "authToken":
* '/api/boards/:boardId/export?authToken=:token'
/**
* @operation export
* @tag Boards
*
* @summary This route is used to export the board **FROM THE APPLICATION**.
*
* @description If user is already logged-in, pass loginToken as param
* "authToken": '/api/boards/:boardId/export?authToken=:token'
*
* See https://blog.kayla.com.au/server-side-route-authentication-in-meteor/
* for detailed explanations
*
* @param {string} boardId the ID of the board we are exporting
* @param {string} authToken the loginToken
*/
JsonRoutes.add('get', '/api/boards/:boardId/export', function(req, res) {
const boardId = req.params.boardId;

View file

@ -1,33 +1,60 @@
Integrations = new Mongo.Collection('integrations');
/**
* Integration with third-party applications
*/
Integrations.attachSchema(new SimpleSchema({
enabled: {
/**
* is the integration enabled?
*/
type: Boolean,
defaultValue: true,
},
title: {
/**
* name of the integration
*/
type: String,
optional: true,
},
type: {
/**
* type of the integratation (Default to 'outgoing-webhooks')
*/
type: String,
defaultValue: 'outgoing-webhooks',
},
activities: {
/**
* activities the integration gets triggered (list)
*/
type: [String],
defaultValue: ['all'],
},
url: { // URL validation regex (https://mathiasbynens.be/demo/url-regex)
/**
* URL validation regex (https://mathiasbynens.be/demo/url-regex)
*/
type: String,
},
token: {
/**
* token of the integration
*/
type: String,
optional: true,
},
boardId: {
/**
* Board ID of the integration
*/
type: String,
},
createdAt: {
/**
* Creation date of the integration
*/
type: Date,
denyUpdate: false,
autoValue() { // eslint-disable-line consistent-return
@ -39,6 +66,9 @@ Integrations.attachSchema(new SimpleSchema({
},
},
userId: {
/**
* user ID who created the interation
*/
type: String,
},
}));
@ -58,7 +88,13 @@ Integrations.allow({
//INTEGRATIONS REST API
if (Meteor.isServer) {
// Get all integrations in board
/**
* @operation get_all_integrations
* @summary Get all integrations in board
*
* @param {string} boardId the board ID
* @return_type [Integrations]
*/
JsonRoutes.add('GET', '/api/boards/:boardId/integrations', function(req, res) {
try {
const paramBoardId = req.params.boardId;
@ -78,7 +114,14 @@ if (Meteor.isServer) {
}
});
// Get a single integration in board
/**
* @operation get_integration
* @summary Get a single integration in board
*
* @param {string} boardId the board ID
* @param {string} intId the integration ID
* @return_type Integrations
*/
JsonRoutes.add('GET', '/api/boards/:boardId/integrations/:intId', function(req, res) {
try {
const paramBoardId = req.params.boardId;
@ -98,7 +141,14 @@ if (Meteor.isServer) {
}
});
// Create a new integration
/**
* @operation new_integration
* @summary Create a new integration
*
* @param {string} boardId the board ID
* @param {string} url the URL of the integration
* @return_type {_id: string}
*/
JsonRoutes.add('POST', '/api/boards/:boardId/integrations', function(req, res) {
try {
const paramBoardId = req.params.boardId;
@ -125,7 +175,19 @@ if (Meteor.isServer) {
}
});
// Edit integration data
/**
* @operation edit_integration
* @summary Edit integration data
*
* @param {string} boardId the board ID
* @param {string} intId the integration ID
* @param {string} [enabled] is the integration enabled?
* @param {string} [title] new name of the integration
* @param {string} [url] new URL of the integration
* @param {string} [token] new token of the integration
* @param {string} [activities] new list of activities of the integration
* @return_type {_id: string}
*/
JsonRoutes.add('PUT', '/api/boards/:boardId/integrations/:intId', function (req, res) {
try {
const paramBoardId = req.params.boardId;
@ -173,7 +235,15 @@ if (Meteor.isServer) {
}
});
// Delete subscribed activities
/**
* @operation delete_integration_activities
* @summary Delete subscribed activities
*
* @param {string} boardId the board ID
* @param {string} intId the integration ID
* @param {string} newActivities the activities to remove from the integration
* @return_type Integrations
*/
JsonRoutes.add('DELETE', '/api/boards/:boardId/integrations/:intId/activities', function (req, res) {
try {
const paramBoardId = req.params.boardId;
@ -197,7 +267,15 @@ if (Meteor.isServer) {
}
});
// Add subscribed activities
/**
* @operation new_integration_activities
* @summary Add subscribed activities
*
* @param {string} boardId the board ID
* @param {string} intId the integration ID
* @param {string} newActivities the activities to add to the integration
* @return_type Integrations
*/
JsonRoutes.add('POST', '/api/boards/:boardId/integrations/:intId/activities', function (req, res) {
try {
const paramBoardId = req.params.boardId;
@ -221,7 +299,14 @@ if (Meteor.isServer) {
}
});
// Delete integration
/**
* @operation delete_integration
* @summary Delete integration
*
* @param {string} boardId the board ID
* @param {string} intId the integration ID
* @return_type {_id: string}
*/
JsonRoutes.add('DELETE', '/api/boards/:boardId/integrations/:intId', function (req, res) {
try {
const paramBoardId = req.params.boardId;

View file

@ -1,10 +1,19 @@
Lists = new Mongo.Collection('lists');
/**
* A list (column) in the Wekan board.
*/
Lists.attachSchema(new SimpleSchema({
title: {
/**
* the title of the list
*/
type: String,
},
archived: {
/**
* is the list archived
*/
type: Boolean,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert && !this.isSet) {
@ -13,9 +22,15 @@ Lists.attachSchema(new SimpleSchema({
},
},
boardId: {
/**
* the board associated to this list
*/
type: String,
},
createdAt: {
/**
* creation date
*/
type: Date,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert) {
@ -26,12 +41,18 @@ Lists.attachSchema(new SimpleSchema({
},
},
sort: {
/**
* is the list sorted
*/
type: Number,
decimal: true,
// XXX We should probably provide a default
optional: true,
},
updatedAt: {
/**
* last update of the list
*/
type: Date,
optional: true,
autoValue() { // eslint-disable-line consistent-return
@ -43,19 +64,31 @@ Lists.attachSchema(new SimpleSchema({
},
},
wipLimit: {
/**
* WIP object, see below
*/
type: Object,
optional: true,
},
'wipLimit.value': {
/**
* value of the WIP
*/
type: Number,
decimal: false,
defaultValue: 1,
},
'wipLimit.enabled': {
/**
* is the WIP enabled
*/
type: Boolean,
defaultValue: false,
},
'wipLimit.soft': {
/**
* is the WIP a soft or hard requirement
*/
type: Boolean,
defaultValue: false,
},
@ -212,6 +245,14 @@ if (Meteor.isServer) {
//LISTS REST API
if (Meteor.isServer) {
/**
* @operation get_all_lists
* @summary Get the list of Lists attached to a board
*
* @param {string} boardId the board ID
* @return_type [{_id: string,
* title: string}]
*/
JsonRoutes.add('GET', '/api/boards/:boardId/lists', function (req, res) {
try {
const paramBoardId = req.params.boardId;
@ -235,6 +276,14 @@ if (Meteor.isServer) {
}
});
/**
* @operation get_list
* @summary Get a List attached to a board
*
* @param {string} boardId the board ID
* @param {string} listId the List ID
* @return_type Lists
*/
JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId', function (req, res) {
try {
const paramBoardId = req.params.boardId;
@ -253,6 +302,14 @@ if (Meteor.isServer) {
}
});
/**
* @operation new_list
* @summary Add a List to a board
*
* @param {string} boardId the board ID
* @param {string} title the title of the List
* @return_type {_id: string}
*/
JsonRoutes.add('POST', '/api/boards/:boardId/lists', function (req, res) {
try {
Authentication.checkUserId( req.userId);
@ -276,6 +333,17 @@ if (Meteor.isServer) {
}
});
/**
* @operation delete_list
* @summary Delete a List
*
* @description This **deletes** a list from a board.
* The list is not put in the recycle bin.
*
* @param {string} boardId the board ID
* @param {string} listId the ID of the list to remove
* @return_type {_id: string}
*/
JsonRoutes.add('DELETE', '/api/boards/:boardId/lists/:listId', function (req, res) {
try {
Authentication.checkUserId( req.userId);

View file

@ -1,10 +1,19 @@
Swimlanes = new Mongo.Collection('swimlanes');
/**
* A swimlane is an line in the kaban board.
*/
Swimlanes.attachSchema(new SimpleSchema({
title: {
/**
* the title of the swimlane
*/
type: String,
},
archived: {
/**
* is the swimlane archived?
*/
type: Boolean,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert && !this.isSet) {
@ -13,9 +22,15 @@ Swimlanes.attachSchema(new SimpleSchema({
},
},
boardId: {
/**
* the ID of the board the swimlane is attached to
*/
type: String,
},
createdAt: {
/**
* creation date of the swimlane
*/
type: Date,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert) {
@ -26,12 +41,18 @@ Swimlanes.attachSchema(new SimpleSchema({
},
},
sort: {
/**
* the sort value of the swimlane
*/
type: Number,
decimal: true,
// XXX We should probably provide a default
optional: true,
},
updatedAt: {
/**
* when was the swimlane last edited
*/
type: Date,
optional: true,
autoValue() { // eslint-disable-line consistent-return
@ -131,6 +152,15 @@ if (Meteor.isServer) {
//SWIMLANE REST API
if (Meteor.isServer) {
/**
* @operation get_all_swimlanes
*
* @summary Get the list of swimlanes attached to a board
*
* @param {string} boardId the ID of the board
* @return_type [{_id: string,
* title: string}]
*/
JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes', function (req, res) {
try {
const paramBoardId = req.params.boardId;
@ -154,6 +184,15 @@ if (Meteor.isServer) {
}
});
/**
* @operation get_swimlane
*
* @summary Get a swimlane
*
* @param {string} boardId the ID of the board
* @param {string} swimlaneId the ID of the swimlane
* @return_type Swimlanes
*/
JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes/:swimlaneId', function (req, res) {
try {
const paramBoardId = req.params.boardId;
@ -172,6 +211,15 @@ if (Meteor.isServer) {
}
});
/**
* @operation new_swimlane
*
* @summary Add a swimlane to a board
*
* @param {string} boardId the ID of the board
* @param {string} title the new title of the swimlane
* @return_type {_id: string}
*/
JsonRoutes.add('POST', '/api/boards/:boardId/swimlanes', function (req, res) {
try {
Authentication.checkUserId( req.userId);
@ -195,6 +243,17 @@ if (Meteor.isServer) {
}
});
/**
* @operation delete_swimlane
*
* @summary Delete a swimlane
*
* @description The swimlane will be deleted, not moved to the recycle bin
*
* @param {string} boardId the ID of the board
* @param {string} swimlaneId the ID of the swimlane
* @return_type {_id: string}
*/
JsonRoutes.add('DELETE', '/api/boards/:boardId/swimlanes/:swimlaneId', function (req, res) {
try {
Authentication.checkUserId( req.userId);

View file

@ -4,8 +4,14 @@ const isSandstorm = Meteor.settings && Meteor.settings.public &&
Meteor.settings.public.sandstorm;
Users = Meteor.users;
/**
* A User in wekan
*/
Users.attachSchema(new SimpleSchema({
username: {
/**
* the username of the user
*/
type: String,
optional: true,
autoValue() { // eslint-disable-line consistent-return
@ -18,17 +24,29 @@ Users.attachSchema(new SimpleSchema({
},
},
emails: {
/**
* the list of emails attached to a user
*/
type: [Object],
optional: true,
},
'emails.$.address': {
/**
* The email address
*/
type: String,
regEx: SimpleSchema.RegEx.Email,
},
'emails.$.verified': {
/**
* Has the email been verified
*/
type: Boolean,
},
createdAt: {
/**
* creation date of the user
*/
type: Date,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert) {
@ -39,6 +57,9 @@ Users.attachSchema(new SimpleSchema({
},
},
profile: {
/**
* profile settings
*/
type: Object,
optional: true,
autoValue() { // eslint-disable-line consistent-return
@ -50,50 +71,86 @@ Users.attachSchema(new SimpleSchema({
},
},
'profile.avatarUrl': {
/**
* URL of the avatar of the user
*/
type: String,
optional: true,
},
'profile.emailBuffer': {
/**
* list of email buffers of the user
*/
type: [String],
optional: true,
},
'profile.fullname': {
/**
* full name of the user
*/
type: String,
optional: true,
},
'profile.hiddenSystemMessages': {
/**
* does the user wants to hide system messages?
*/
type: Boolean,
optional: true,
},
'profile.initials': {
/**
* initials of the user
*/
type: String,
optional: true,
},
'profile.invitedBoards': {
/**
* board IDs the user has been invited to
*/
type: [String],
optional: true,
},
'profile.language': {
/**
* language of the user
*/
type: String,
optional: true,
},
'profile.notifications': {
/**
* enabled notifications for the user
*/
type: [String],
optional: true,
},
'profile.showCardsCountAt': {
/**
* showCardCountAt field of the user
*/
type: Number,
optional: true,
},
'profile.starredBoards': {
/**
* list of starred board IDs
*/
type: [String],
optional: true,
},
'profile.icode': {
/**
* icode
*/
type: String,
optional: true,
},
'profile.boardView': {
/**
* boardView field of the user
*/
type: String,
optional: true,
allowedValues: [
@ -103,27 +160,45 @@ Users.attachSchema(new SimpleSchema({
],
},
services: {
/**
* services field of the user
*/
type: Object,
optional: true,
blackbox: true,
},
heartbeat: {
/**
* last time the user has been seen
*/
type: Date,
optional: true,
},
isAdmin: {
/**
* is the user an admin of the board?
*/
type: Boolean,
optional: true,
},
createdThroughApi: {
/**
* was the user created through the API?
*/
type: Boolean,
optional: true,
},
loginDisabled: {
/**
* loginDisabled field of the user
*/
type: Boolean,
optional: true,
},
'authenticationMethod': {
/**
* authentication method of the user
*/
type: String,
optional: false,
defaultValue: 'password',
@ -681,6 +756,12 @@ if (Meteor.isServer) {
}
});
/**
* @operation get_current_user
*
* @summary returns the current user
* @return_type Users
*/
JsonRoutes.add('GET', '/api/user', function(req, res) {
try {
Authentication.checkLoggedIn(req.userId);
@ -699,6 +780,15 @@ if (Meteor.isServer) {
}
});
/**
* @operation get_all_users
*
* @summary return all the users
*
* @description Only the admin user (the first user) can call the REST API.
* @return_type [{ _id: string,
* username: string}]
*/
JsonRoutes.add('GET', '/api/users', function (req, res) {
try {
Authentication.checkUserId(req.userId);
@ -717,6 +807,16 @@ if (Meteor.isServer) {
}
});
/**
* @operation get_user
*
* @summary get a given user
*
* @description Only the admin user (the first user) can call the REST API.
*
* @param {string} userId the user ID
* @return_type Users
*/
JsonRoutes.add('GET', '/api/users/:userId', function (req, res) {
try {
Authentication.checkUserId(req.userId);
@ -734,6 +834,23 @@ if (Meteor.isServer) {
}
});
/**
* @operation edit_user
*
* @summary edit a given user
*
* @description Only the admin user (the first user) can call the REST API.
*
* Possible values for *action*:
* - `takeOwnership`: The admin takes the ownership of ALL boards of the user (archived and not archived) where the user is admin on.
* - `disableLogin`: Disable a user (the user is not allowed to login and his login tokens are purged)
* - `enableLogin`: Enable a user
*
* @param {string} userId the user ID
* @param {string} action the action
* @return_type {_id: string,
* title: string}
*/
JsonRoutes.add('PUT', '/api/users/:userId', function (req, res) {
try {
Authentication.checkUserId(req.userId);
@ -777,6 +894,25 @@ if (Meteor.isServer) {
}
});
/**
* @operation add_board_member
* @tag Boards
*
* @summary Add New Board Member with Role
*
* @description Only the admin user (the first user) can call the REST API.
*
* **Note**: see [Boards.set_board_member_permission](#set_board_member_permission)
* to later change the permissions.
*
* @param {string} boardId the board ID
* @param {string} userId the user ID
* @param {boolean} isAdmin is the user an admin of the board
* @param {boolean} isNoComments disable comments
* @param {boolean} isCommentOnly only enable comments
* @return_type {_id: string,
* title: string}
*/
JsonRoutes.add('POST', '/api/boards/:boardId/members/:userId/add', function (req, res) {
try {
Authentication.checkUserId(req.userId);
@ -817,6 +953,20 @@ if (Meteor.isServer) {
}
});
/**
* @operation remove_board_member
* @tag Boards
*
* @summary Remove Member from Board
*
* @description Only the admin user (the first user) can call the REST API.
*
* @param {string} boardId the board ID
* @param {string} userId the user ID
* @param {string} action the action (needs to be `remove`)
* @return_type {_id: string,
* title: string}
*/
JsonRoutes.add('POST', '/api/boards/:boardId/members/:userId/remove', function (req, res) {
try {
Authentication.checkUserId(req.userId);
@ -852,6 +1002,18 @@ if (Meteor.isServer) {
}
});
/**
* @operation new_user
*
* @summary Create a new user
*
* @description Only the admin user (the first user) can call the REST API.
*
* @param {string} username the new username
* @param {string} email the email of the new user
* @param {string} password the password of the new user
* @return_type {_id: string}
*/
JsonRoutes.add('POST', '/api/users/', function (req, res) {
try {
Authentication.checkUserId(req.userId);
@ -876,6 +1038,16 @@ if (Meteor.isServer) {
}
});
/**
* @operation delete_user
*
* @summary Delete a user
*
* @description Only the admin user (the first user) can call the REST API.
*
* @param {string} userId the ID of the user to delete
* @return_type {_id: string}
*/
JsonRoutes.add('DELETE', '/api/users/:userId', function (req, res) {
try {
Authentication.checkUserId(req.userId);

27
openapi/README.md Normal file
View file

@ -0,0 +1,27 @@
# OpenAPI tools and doc generation
## Open API generation
This folder contains a script (`generate_openapi.py`) that extracts
the REST API of Wekan and exports it under the OpenAPI 2.0 specification
(Swagger 2.0).
### dependencies
- python3
- [esprima-python](https://github.com/Kronuz/esprima-python)
### calling the tool
python3 generate_openapi.py --release v1.65 > ../public/wekan_api.yml
## Generating docs
Now that we have the OpenAPI, it's easy enough to convert the YAML file into some nice Markdown with
[shins](https://github.com/Mermade/shins) and [api2html](https://github.com/tobilg/api2html),
or even [ReDoc](https://github.com/Rebilly/ReDoc):
api2html -c ../public/wekan-logo-header.png -o api.html ../public/wekan_api.yml
or
redoc-cli serve ../public/wekan_api.yml

915
openapi/generate_openapi.py Normal file
View file

@ -0,0 +1,915 @@
#!/bin/env python3
import argparse
import esprima
import json
import os
import re
import sys
def get_req_body_elems(obj, elems):
if obj.type == 'FunctionExpression':
get_req_body_elems(obj.body, elems)
elif obj.type == 'BlockStatement':
for s in obj.body:
get_req_body_elems(s, elems)
elif obj.type == 'TryStatement':
get_req_body_elems(obj.block, elems)
elif obj.type == 'ExpressionStatement':
get_req_body_elems(obj.expression, elems)
elif obj.type == 'MemberExpression':
left = get_req_body_elems(obj.object, elems)
right = obj.property.name
if left == 'req.body' and right not in elems:
elems.append(right)
return '{}.{}'.format(left, right)
elif obj.type == 'VariableDeclaration':
for s in obj.declarations:
get_req_body_elems(s, elems)
elif obj.type == 'VariableDeclarator':
if obj.id.type == 'ObjectPattern':
# get_req_body_elems() can't be called directly here:
# const {isAdmin, isNoComments, isCommentOnly} = req.body;
right = get_req_body_elems(obj.init, elems)
if right == 'req.body':
for p in obj.id.properties:
name = p.key.name
if name not in elems:
elems.append(name)
else:
get_req_body_elems(obj.init, elems)
elif obj.type == 'Property':
get_req_body_elems(obj.value, elems)
elif obj.type == 'ObjectExpression':
for s in obj.properties:
get_req_body_elems(s, elems)
elif obj.type == 'CallExpression':
for s in obj.arguments:
get_req_body_elems(s, elems)
elif obj.type == 'ArrayExpression':
for s in obj.elements:
get_req_body_elems(s, elems)
elif obj.type == 'IfStatement':
get_req_body_elems(obj.test, elems)
if obj.consequent is not None:
get_req_body_elems(obj.consequent, elems)
if obj.alternate is not None:
get_req_body_elems(obj.alternate, elems)
elif obj.type in ('LogicalExpression', 'BinaryExpression', 'AssignmentExpression'):
get_req_body_elems(obj.left, elems)
get_req_body_elems(obj.right, elems)
elif obj.type in ('ReturnStatement', 'UnaryExpression'):
get_req_body_elems(obj.argument, elems)
elif obj.type == 'Literal':
pass
elif obj.type == 'Identifier':
return obj.name
elif obj.type == 'FunctionDeclaration':
pass
else:
print(obj)
return ''
def cleanup_jsdocs(jsdoc):
# remove leading spaces before the first '*'
doc = [s.lstrip() for s in jsdoc.value.split('\n')]
# remove leading stars
doc = [s.lstrip('*') for s in doc]
# remove leading empty lines
while len(doc) and not doc[0].strip():
doc.pop(0)
# remove terminating empty lines
while len(doc) and not doc[-1].strip():
doc.pop(-1)
return doc
class JS2jsonDecoder(json.JSONDecoder):
def decode(self, s):
result = super().decode(s) # result = super(Decoder, self).decode(s) for Python 2.x
return self._decode(result)
def _decode(self, o):
if isinstance(o, str) or isinstance(o, unicode):
try:
return int(o)
except ValueError:
return o
elif isinstance(o, dict):
return {k: self._decode(v) for k, v in o.items()}
elif isinstance(o, list):
return [self._decode(v) for v in o]
else:
return o
def load_return_type_jsdoc_json(data):
regex_replace = [(r'\n', r' '), # replace new lines by spaces
(r'([\{\s,])(\w+)(:)', r'\1"\2"\3'), # insert double quotes in keys
(r'(:)\s*([^:\},\]]+)\s*([\},\]])', r'\1"\2"\3'), # insert double quotes in values
(r'(\[)\s*([^{].+)\s*(\])', r'\1"\2"\3'), # insert double quotes in array items
(r'^\s*([^\[{].+)\s*', r'"\1"')] # insert double quotes in single item
for r, s in regex_replace:
data = re.sub(r, s, data)
return json.loads(data)
class EntryPoint(object):
def __init__(self, schema, statements):
self.schema = schema
self.method, self._path, self.body = statements
self._jsdoc = None
self._doc = {}
self._raw_doc = None
self.path = self.compute_path()
self.method_name = self.method.value.lower()
self.body_params = []
if self.method_name in ('post', 'put'):
get_req_body_elems(self.body, self.body_params)
# replace the :parameter in path by {parameter}
self.url = re.sub(r':([^/]*)Id', r'{\1}', self.path)
self.url = re.sub(r':([^/]*)', r'{\1}', self.url)
# reduce the api name
# get_boards_board_cards() should be get_board_cards()
tokens = self.url.split('/')
reduced_function_name = []
for i, token in enumerate(tokens):
if token in ('api'):
continue
if (i < len(tokens) - 1 and # not the last item
tokens[i + 1].startswith('{')): # and the next token is a parameter
continue
reduced_function_name.append(token.strip('{}'))
self.reduced_function_name = '_'.join(reduced_function_name)
# mark the schema as used
schema.used = True
def compute_path(self):
return self._path.value.rstrip('/')
def error(self, message):
if self._raw_doc is None:
sys.stderr.write('in {},\n'.format(self.schema.name))
sys.stderr.write('{}\n'.format(message))
return
sys.stderr.write('in {}, lines {}-{}\n'.format(self.schema.name,
self._raw_doc.loc.start.line,
self._raw_doc.loc.end.line))
sys.stderr.write('{}\n'.format(self._raw_doc.value))
sys.stderr.write('{}\n'.format(message))
@property
def doc(self):
return self._doc
@doc.setter
def doc(self, doc):
'''Parse the JSDoc attached to an entry point.
`jsdoc` will not get these right as they are not attached to a method.
So instead, we do our custom parsing here (yes, subject to errors).
The expected format is the following (empty lines between entries
are ignored):
/**
* @operation name_of_entry_point
* @tag: a_tag_to_add
* @tag: an_other_tag_to_add
* @summary A nice summary, better in one line.
*
* @description This is a quite long description.
* We can use *mardown* as the final rendering is done
* by slate.
*
* indentation doesn't matter.
*
* @param param_0 description of param 0
* @param {string} param_1 we can also put the type of the parameter
* before its name, like in JSDoc
* @param {boolean} [param_2] we can also tell if the parameter is
* optional by adding square brackets around its name
*
* @return Documents a return value
*/
Notes:
- name_of_entry_point will be referenced in the ToC of the generated
document. This is also the operationId used in the resulting openapi
file. It needs to be uniq in the namesapce (the current schema.js
file)
- tags are appended to the current Schema attached to the file
'''
self._raw_doc = doc
self._jsdoc = cleanup_jsdocs(doc)
def store_tag(tag, data):
# check that there is something to store first
if not data.strip():
return
# remove terminating whitespaces and empty lines
data = data.rstrip()
# parameters are handled specially
if tag == 'param':
if 'params' not in self._doc:
self._doc['params'] = {}
params = self._doc['params']
param_type = None
try:
name, desc = data.split(maxsplit=1)
except ValueError:
desc = ''
if name.startswith('{'):
param_type = name.strip('{}')
if param_type not in ['string', 'number', 'boolean', 'integer', 'array', 'file']:
self.error('Warning, unknown type {}\n allowed values: string, number, boolean, integer, array, file'.format(param_type))
try:
name, desc = desc.split(maxsplit=1)
except ValueError:
desc = ''
optional = name.startswith('[') and name.endswith(']')
if optional:
name = name[1:-1]
# we should not have 2 identical parameter names
if tag in params:
self.error('Warning, overwriting parameter {}'.format(name))
params[name] = (param_type, optional, desc)
if name.endswith('Id'):
# we strip out the 'Id' from the form parameters, we need
# to keep the actual description around
name = name[:-2]
if name not in params:
params[name] = (param_type, optional, desc)
return
# 'tag' can be set several times
if tag == 'tag':
if tag not in self._doc:
self._doc[tag] = []
self._doc[tag].append(data)
return
# 'return' tag is json
if tag == 'return_type':
try:
data = load_return_type_jsdoc_json(data)
except json.decoder.JSONDecodeError:
pass
# we should not have 2 identical tags but @param or @tag
if tag in self._doc:
self.error('Warning, overwriting tag {}'.format(tag))
self._doc[tag] = data
# reset the current doc fields
self._doc = {}
# first item is supposed to be the description
current_tag = 'description'
current_data = ''
for line in self._jsdoc:
if line.lstrip().startswith('@'):
tag, data = line.lstrip().split(maxsplit=1)
if tag in ['@operation', '@summary', '@description', '@param', '@return_type', '@tag']:
# store the current data
store_tag(current_tag, current_data)
current_tag = tag.lstrip('@')
current_data = ''
line = data
else:
self.error('Unknown tag {}, ignoring'.format(tag))
current_data += line + '\n'
store_tag(current_tag, current_data)
@property
def summary(self):
if 'summary' in self._doc:
# new lines are not allowed
return self._doc['summary'].replace('\n', ' ')
return None
def doc_param(self, name):
if 'params' in self._doc and name in self._doc['params']:
return self._doc['params'][name]
return None, None, None
def print_openapi_param(self, name, indent):
ptype, poptional, pdesc = self.doc_param(name)
if pdesc is not None:
print('{}description: |'.format(' ' * indent))
print('{}{}'.format(' ' * (indent + 2), pdesc))
else:
print('{}description: the {} value'.format(' ' * indent, name))
if ptype is not None:
print('{}type: {}'.format(' ' * indent, ptype))
else:
print('{}type: string'.format(' ' * indent))
if poptional:
print('{}required: false'.format(' ' * indent))
else:
print('{}required: true'.format(' ' * indent))
@property
def operationId(self):
if 'operation' in self._doc:
return self._doc['operation']
return '{}_{}'.format(self.method_name, self.reduced_function_name)
@property
def description(self):
if 'description' in self._doc:
return self._doc['description']
return None
@property
def returns(self):
if 'return_type' in self._doc:
return self._doc['return_type']
return None
@property
def tags(self):
tags = []
if self.schema.fields is not None:
tags.append(self.schema.name)
if 'tag' in self._doc:
tags.extend(self._doc['tag'])
return tags
def print_openapi_return(self, obj, indent):
if isinstance(obj, dict):
print('{}type: object'.format(' ' * indent))
print('{}properties:'.format(' ' * indent))
for k, v in obj.items():
print('{}{}:'.format(' ' * (indent + 2), k))
self.print_openapi_return(v, indent + 4)
elif isinstance(obj, list):
if len(obj) > 1:
self.error('Error while parsing @return tag, an array should have only one type')
print('{}type: array'.format(' ' * indent))
print('{}items:'.format(' ' * indent))
self.print_openapi_return(obj[0], indent + 2)
elif isinstance(obj, str) or isinstance(obj, unicode):
rtype = 'type: ' + obj
if obj == self.schema.name:
rtype = '$ref: "#/definitions/{}"'.format(obj)
print('{}{}'.format(' ' * indent, rtype))
def print_openapi(self):
parameters = [token[1:-2] if token.endswith('Id') else token[1:]
for token in self.path.split('/')
if token.startswith(':')]
print(' {}:'.format(self.method_name))
print(' operationId: {}'.format(self.operationId))
if self.summary is not None:
print(' summary: {}'.format(self.summary))
if self.description is not None:
print(' description: |')
for line in self.description.split('\n'):
if line.strip():
print(' {}'.format(line))
else:
print('')
if len(self.tags) > 0:
print(' tags:')
for tag in self.tags:
print(' - {}'.format(tag))
# export the parameters
if self.method_name in ('post', 'put'):
print(''' consumes:
- multipart/form-data
- application/json''')
if len(parameters) > 0 or self.method_name in ('post', 'put'):
print(' parameters:')
if self.method_name in ('post', 'put'):
for f in self.body_params:
print(''' - name: {}
in: formData'''.format(f))
self.print_openapi_param(f, 10)
for p in parameters:
if p in self.body_params:
self.error(' '.join((p, self.path, self.method_name)))
print(''' - name: {}
in: path'''.format(p))
self.print_openapi_param(p, 10)
print(''' produces:
- application/json
security:
- UserSecurity: []
responses:
'200':
description: |-
200 response''')
if self.returns is not None:
print(' schema:')
self.print_openapi_return(self.returns, 12)
class SchemaProperty(object):
def __init__(self, statement, schema):
self.schema = schema
self.statement = statement
self.name = statement.key.name or statement.key.value
self.type = 'object'
self.blackbox = False
self.required = True
for p in statement.value.properties:
if p.key.name == 'type':
if p.value.type == 'Identifier':
self.type = p.value.name.lower()
elif p.value.type == 'ArrayExpression':
self.type = 'array'
self.elements = [e.name.lower() for e in p.value.elements]
elif p.key.name == 'allowedValues':
self.type = 'enum'
self.enum = [e.value.lower() for e in p.value.elements]
elif p.key.name == 'blackbox':
self.blackbox = True
elif p.key.name == 'optional' and p.value.value:
self.required = False
self._doc = None
self._raw_doc = None
@property
def doc(self):
return self._doc
@doc.setter
def doc(self, jsdoc):
self._raw_doc = jsdoc
self._doc = cleanup_jsdocs(jsdoc)
def process_jsdocs(self, jsdocs):
start = self.statement.key.loc.start.line
for index, doc in enumerate(jsdocs):
if start + 1 == doc.loc.start.line:
self.doc = doc
jsdocs.pop(index)
return
def __repr__(self):
return 'SchemaProperty({}{}, {})'.format(self.name,
'*' if self.required else '',
self.doc)
def print_openapi(self, indent, current_schema, required_properties):
schema_name = self.schema.name
name = self.name
# deal with subschemas
if '.' in name:
if name.endswith('$'):
# reference in reference
subschema = ''.join([n.capitalize() for n in self.name.split('.')[:-1]])
subschema = self.schema.name + subschema
if current_schema != subschema:
if required_properties is not None and required_properties:
print(' required:')
for f in required_properties:
print(' - {}'.format(f))
required_properties.clear()
print(''' {}:
type: object'''.format(subschema))
return current_schema
subschema = name.split('.')[0]
schema_name = self.schema.name + subschema.capitalize()
name = name.split('.')[-1]
if current_schema != schema_name:
if required_properties is not None and required_properties:
print(' required:')
for f in required_properties:
print(' - {}'.format(f))
required_properties.clear()
print(''' {}:
type: object
properties:'''.format(schema_name))
if required_properties is not None and self.required:
required_properties.append(name)
print('{}{}:'.format(' ' * indent, name))
if self.doc is not None:
print('{} description: |'.format(' ' * indent))
for line in self.doc:
if line.strip():
print('{} {}'.format(' ' * indent, line))
else:
print('')
ptype = self.type
if ptype in ('enum', 'date'):
ptype = 'string'
if ptype != 'object':
print('{} type: {}'.format(' ' * indent, ptype))
if self.type == 'array':
print('{} items:'.format(' ' * indent))
for elem in self.elements:
if elem == 'object':
print('{} $ref: "#/definitions/{}"'.format(' ' * indent, schema_name + name.capitalize()))
else:
print('{} type: {}'.format(' ' * indent, elem))
if not self.required:
print('{} x-nullable: true'.format(' ' * indent))
elif self.type == 'object':
if self.blackbox:
print('{} type: object'.format(' ' * indent))
else:
print('{} $ref: "#/definitions/{}"'.format(' ' * indent, schema_name + name.capitalize()))
elif self.type == 'enum':
print('{} enum:'.format(' ' * indent))
for enum in self.enum:
print('{} - {}'.format(' ' * indent, enum))
if '.' not in self.name and not self.required:
print('{} x-nullable: true'.format(' ' * indent))
return schema_name
class Schemas(object):
def __init__(self, data=None, jsdocs=None, name=None):
self.name = name
self._data = data
self.fields = None
self.used = False
if data is not None:
if self.name is None:
self.name = data.expression.callee.object.name
content = data.expression.arguments[0].arguments[0]
self.fields = [SchemaProperty(p, self) for p in content.properties]
self._doc = None
self._raw_doc = None
if jsdocs is not None:
self.process_jsdocs(jsdocs)
@property
def doc(self):
if self._doc is None:
return None
return ' '.join(self._doc)
@doc.setter
def doc(self, jsdoc):
self._raw_doc = jsdoc
self._doc = cleanup_jsdocs(jsdoc)
def process_jsdocs(self, jsdocs):
start = self._data.loc.start.line
end = self._data.loc.end.line
for doc in jsdocs:
if doc.loc.end.line + 1 == start:
self.doc = doc
docs = [doc
for doc in jsdocs
if doc.loc.start.line >= start and doc.loc.end.line <= end]
for field in self.fields:
field.process_jsdocs(docs)
def print_openapi(self):
# empty schemas are skipped
if self.fields is None:
return
print(' {}:'.format(self.name))
print(' type: object')
if self.doc is not None:
print(' description: {}'.format(self.doc))
print(' properties:')
# first print out the object itself
properties = [field for field in self.fields if '.' not in field.name]
for prop in properties:
prop.print_openapi(6, None, None)
required_properties = [f.name for f in properties if f.required]
if required_properties:
print(' required:')
for f in required_properties:
print(' - {}'.format(f))
# then print the references
current = None
required_properties = []
properties = [f for f in self.fields if '.' in f.name and not f.name.endswith('$')]
for prop in properties:
current = prop.print_openapi(6, current, required_properties)
if required_properties:
print(' required:')
for f in required_properties:
print(' - {}'.format(f))
required_properties = []
# then print the references in the references
for prop in [f for f in self.fields if '.' in f.name and f.name.endswith('$')]:
current = prop.print_openapi(6, current, required_properties)
if required_properties:
print(' required:')
for f in required_properties:
print(' - {}'.format(f))
def parse_schemas(schemas_dir):
schemas = {}
entry_points = []
for root, dirs, files in os.walk(schemas_dir):
files.sort()
for filename in files:
path = os.path.join(root, filename)
with open(path) as f:
data = ''.join(f.readlines())
try:
# if the file failed, it's likely it doesn't contain a schema
program = esprima.parseScript(data, options={'comment': True, 'loc': True})
except:
continue
current_schema = None
jsdocs = [c for c in program.comments
if c.type == 'Block' and c.value.startswith('*\n')]
for statement in program.body:
# find the '<ITEM>.attachSchema(new SimpleSchema(<data>)'
# those are the schemas
if (statement.type == 'ExpressionStatement' and
statement.expression.callee is not None and
statement.expression.callee.property is not None and
statement.expression.callee.property.name == 'attachSchema' and
statement.expression.arguments[0].type == 'NewExpression' and
statement.expression.arguments[0].callee.name == 'SimpleSchema'):
schema = Schemas(statement, jsdocs)
current_schema = schema.name
schemas[current_schema] = schema
# find all the 'if (Meteor.isServer) { JsonRoutes.add('
# those are the entry points of the API
elif (statement.type == 'IfStatement' and
statement.test.type == 'MemberExpression' and
statement.test.object.name == 'Meteor' and
statement.test.property.name == 'isServer'):
data = [s.expression.arguments
for s in statement.consequent.body
if (s.type == 'ExpressionStatement' and
s.expression.type == 'CallExpression' and
s.expression.callee.object.name == 'JsonRoutes')]
# we found at least one entry point, keep them
if len(data) > 0:
if current_schema is None:
current_schema = filename
schemas[current_schema] = Schemas(name=current_schema)
schema_entry_points = [EntryPoint(schemas[current_schema], d)
for d in data]
entry_points.extend(schema_entry_points)
# try to match JSDoc to the operations
for entry_point in schema_entry_points:
operation = entry_point.method # POST/GET/PUT/DELETE
jsdoc = [j for j in jsdocs
if j.loc.end.line + 1 == operation.loc.start.line]
if bool(jsdoc):
entry_point.doc = jsdoc[0]
return schemas, entry_points
def generate_openapi(schemas, entry_points, version):
print('''swagger: '2.0'
info:
title: Wekan REST API
version: {0}
description: |
The REST API allows you to control and extend Wekan with ease.
If you are an end-user and not a dev or a tester, [create an issue](https://github.com/wekan/wekan/issues/new) to request new APIs.
> All API calls in the documentation are made using `curl`. However, you are free to use Java / Python / PHP / Golang / Ruby / Swift / Objective-C / Rust / Scala / C# or any other programming languages.
# Production Security Concerns
When calling a production Wekan server, ensure it is running via HTTPS and has a valid SSL Certificate. The login method requires you to post your username and password in plaintext, which is why we highly suggest only calling the REST login api over HTTPS. Also, few things to note:
* Only call via HTTPS
* Implement a timed authorization token expiration strategy
* Ensure the calling user only has permissions for what they are calling and no more
schemes:
- http
securityDefinitions:
UserSecurity:
type: apiKey
in: header
name: Authorization
paths:
/users/login:
post:
operationId: login
summary: Login with REST API
consumes:
- application/x-www-form-urlencoded
- application/json
tags:
- Login
parameters:
- name: username
in: formData
required: true
description: |
Your username
type: string
- name: password
in: formData
required: true
description: |
Your password
type: string
format: password
responses:
200:
description: |-
Successful authentication
schema:
items:
properties:
id:
type: string
token:
type: string
tokenExpires:
type: string
400:
description: |
Error in authentication
schema:
items:
properties:
error:
type: number
reason:
type: string
default:
description: |
Error in authentication
/users/register:
post:
operationId: register
summary: Register with REST API
description: |
Notes:
- You will need to provide the token for any of the authenticated methods.
consumes:
- application/x-www-form-urlencoded
- application/json
tags:
- Login
parameters:
- name: username
in: formData
required: true
description: |
Your username
type: string
- name: password
in: formData
required: true
description: |
Your password
type: string
format: password
- name: email
in: formData
required: true
description: |
Your email
type: string
responses:
200:
description: |-
Successful registration
schema:
items:
properties:
id:
type: string
token:
type: string
tokenExpires:
type: string
400:
description: |
Error in registration
schema:
items:
properties:
error:
type: number
reason:
type: string
default:
description: |
Error in registration
'''.format(version))
# GET and POST on the same path are valid, we need to reshuffle the paths
# with the path as the sorting key
methods = {}
for ep in entry_points:
if ep.path not in methods:
methods[ep.path] = []
methods[ep.path].append(ep)
sorted_paths = list(methods.keys())
sorted_paths.sort()
for path in sorted_paths:
print(' {}:'.format(methods[path][0].url))
for ep in methods[path]:
ep.print_openapi()
print('definitions:')
for schema in schemas.values():
# do not export the objects if there is no API attached
if not schema.used:
continue
schema.print_openapi()
def main():
parser = argparse.ArgumentParser(description='Generate an OpenAPI 2.0 from the given JS schemas.')
script_dir = os.path.dirname(os.path.realpath(__file__))
parser.add_argument('--release', default='git-master', nargs=1,
help='the current version of the API, can be retrieved by running `git describe --tags --abbrev=0`')
parser.add_argument('dir', default='{}/../models'.format(script_dir), nargs='?',
help='the directory where to look for schemas')
args = parser.parse_args()
schemas, entry_points = parse_schemas(args.dir)
generate_openapi(schemas, entry_points, args.release[0])
if __name__ == '__main__':
main()

View file

@ -90,15 +90,34 @@ parts:
- ca-certificates
- apt-utils
- python
- python3
- g++
- capnproto
- curl
- execstack
- nodejs
- npm
stage-packages:
- libfontconfig1
override-build: |
echo "Cleaning environment first"
rm -rf ~/.meteor ~/.npm /usr/local/lib/node_modules
# Create the OpenAPI specification
rm -rf .build
mkdir -p .build/python
cd .build/python
git clone --depth 1 -b master git://github.com/Kronuz/esprima-python
cd esprima-python
python3 setup.py install
cd ../../..
mkdir -p ./public/api
python3 ./openapi/generate_openapi.py --release $(git describe --tags --abbrev=0) > ./public/api/wekan.yml
# we temporary need api2html and mkdirp
npm install -g api2html
npm install -g mkdirp
api2html -c ./public/wekan-logo-header.png -o ./public/api/wekan.html ./public/api/wekan.yml
npm uninstall -g mkdirp
npm uninstall -g api2html
# Node Fibers 100% CPU usage issue:
# https://github.com/wekan/wekan-mongodb/issues/2#issuecomment-381453161
# https://github.com/meteor/meteor/issues/9796#issuecomment-381676326