From 6592102e8ffbccf7cecac519d44a61d405df9de4 Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Tue, 14 Oct 2025 11:56:11 +0300 Subject: [PATCH] v8.02 --- CHANGELOG.md | 2 +- Dockerfile | 6 +- Stackerfile.yml | 2 +- docs/Platforms/Propietary/Windows/Offline.md | 4 +- openapi/generate_openapi.js | 406 +++++++++++++++++++ package-lock.json | 2 +- package.json | 2 +- public/api/wekan.yml | 330 ++++++++++++++- releases/rebuild-docs.sh | 66 +-- sandstorm-pkgdef.capnp | 4 +- snapcraft.yaml | 8 +- 11 files changed, 789 insertions(+), 43 deletions(-) create mode 100644 openapi/generate_openapi.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 07e9e8447..9853a62b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ Fixing other platforms In Progress. [Upgrade WeKan](https://wekan.fi/upgrade/) -# Upcoming WeKan ® release +# v8.02 2025-10-14 WeKan ® release This release adds the following new features: diff --git a/Dockerfile b/Dockerfile index 0612f3149..d04e33e2b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -249,9 +249,9 @@ cd /home/wekan/app # Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc. #rm -rf /home/wekan/app_build/bundle/programs/web.browser.legacy #mv /home/wekan/app_build/bundle /build -wget "https://github.com/wekan/wekan/releases/download/v8.01/wekan-8.01-amd64.zip" -unzip wekan-8.01-amd64.zip -rm wekan-8.01-amd64.zip +wget "https://github.com/wekan/wekan/releases/download/v8.02/wekan-8.02-amd64.zip" +unzip wekan-8.02-amd64.zip +rm wekan-8.02-amd64.zip mv /home/wekan/app/bundle /build # Put back the original tar diff --git a/Stackerfile.yml b/Stackerfile.yml index 55e0e0a16..fcaf9c9c3 100644 --- a/Stackerfile.yml +++ b/Stackerfile.yml @@ -1,5 +1,5 @@ appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928 -appVersion: "v8.01.0" +appVersion: "v8.02.0" files: userUploads: - README.md diff --git a/docs/Platforms/Propietary/Windows/Offline.md b/docs/Platforms/Propietary/Windows/Offline.md index fd453eefb..a549d75ec 100644 --- a/docs/Platforms/Propietary/Windows/Offline.md +++ b/docs/Platforms/Propietary/Windows/Offline.md @@ -10,7 +10,7 @@ This is without container (without Docker or Snap). Right click and download files 1-4: -1. [wekan-8.01-amd64-windows.zip](https://github.com/wekan/wekan/releases/download/v8.01/wekan-8.01-amd64-windows.zip) +1. [wekan-8.02-amd64-windows.zip](https://github.com/wekan/wekan/releases/download/v8.02/wekan-8.02-amd64-windows.zip) 2. [node.exe](https://nodejs.org/dist/latest-v14.x/win-x64/node.exe) @@ -22,7 +22,7 @@ Right click and download files 1-4: 6. Double click `mongodb-windows-x86_64-7.0.25-signed.msi` . In installer, uncheck downloading MongoDB compass. -7. Unzip `wekan-8.01-amd64-windows.zip` , inside it is directory `bundle`, to it copy other files: +7. Unzip `wekan-8.02-amd64-windows.zip` , inside it is directory `bundle`, to it copy other files: ``` bundle (directory) diff --git a/openapi/generate_openapi.js b/openapi/generate_openapi.js new file mode 100644 index 000000000..2e06eebe9 --- /dev/null +++ b/openapi/generate_openapi.js @@ -0,0 +1,406 @@ +#!/usr/bin/env node +/* + Node.js port of openapi/generate_openapi.py (minimal, Node 14 compatible). + Parses models to produce an OpenAPI 2.0 YAML on stdout. +*/ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const esprima = require('esprima'); + +function cleanupJsdocs(jsdoc) { + const lines = jsdoc.value.split('\n') + .map(s => s.replace(/^\s*/, '')) + .map(s => s.replace(/^\*/, '')); + while (lines.length && !lines[0].trim()) lines.shift(); + while (lines.length && !lines[lines.length - 1].trim()) lines.pop(); + return lines; +} + +function loadReturnTypeJsdocJson(data) { + let s = data; + const repl = [ + [/\n/g, ' '], + [/([\{\s,])(\w+)(:)/g, '$1"$2"$3'], + [/(:)\s*([^:\},\]]+)\s*([\},\]])/g, '$1"$2"$3'], + [/([\[])\s*([^\{].+)\s*(\])/g, '$1"$2"$3'], + [/^\s*([^\[{].+)\s*/, '"$1"'] + ]; + for (const [r, rep] of repl) s = s.replace(r, rep); + try { return JSON.parse(s); } catch { return data; } +} + +class Context { + constructor(filePath) { + this.path = filePath; + this._txt = fs.readFileSync(filePath, 'utf8').split(/\r?\n/); + const data = this._txt.join('\n'); + this.program = esprima.parseModule(data, { comment: true, loc: true, range: true }); + } + textAt(begin, end) { return this._txt.slice(begin - 1, end).join('\n'); } +} + +function parseFile(filePath) { + try { return new Context(filePath); } catch { return undefined; } +} + +function getReqBodyElems(node, acc) { + if (!node) return ''; + switch (node.type) { + case 'FunctionExpression': + case 'ArrowFunctionExpression': return getReqBodyElems(node.body, acc); + case 'BlockStatement': node.body.forEach(s => getReqBodyElems(s, acc)); return ''; + case 'TryStatement': return getReqBodyElems(node.block, acc); + case 'ExpressionStatement': return getReqBodyElems(node.expression, acc); + case 'MemberExpression': { + const left = getReqBodyElems(node.object, acc); + const right = node.property && node.property.name; + if (left === 'req.body' && right && !acc.includes(right)) acc.push(right); + return `${left}.${right}`; + } + case 'VariableDeclaration': node.declarations.forEach(s => getReqBodyElems(s, acc)); return ''; + case 'VariableDeclarator': return getReqBodyElems(node.init, acc); + case 'Property': return getReqBodyElems(node.value, acc); + case 'ObjectExpression': node.properties.forEach(s => getReqBodyElems(s, acc)); return ''; + case 'CallExpression': node.arguments.forEach(s => getReqBodyElems(s, acc)); return ''; + case 'ArrayExpression': node.elements.forEach(s => getReqBodyElems(s, acc)); return ''; + case 'IfStatement': getReqBodyElems(node.test, acc); if (node.consequent) getReqBodyElems(node.consequent, acc); if (node.alternate) getReqBodyElems(node.alternate, acc); return ''; + case 'LogicalExpression': + case 'BinaryExpression': + case 'AssignmentExpression': getReqBodyElems(node.left, acc); getReqBodyElems(node.right, acc); return ''; + case 'ChainExpression': return getReqBodyElems(node.expression, acc); + case 'ReturnStatement': + case 'UnaryExpression': if (node.argument) return getReqBodyElems(node.argument, acc); return ''; + case 'Identifier': return node.name; + default: return ''; + } +} + +class EntryPoint { + constructor(schema, [method, pathLit, body]) { + this.schema = schema; + this.method = method; this._path = pathLit; this.body = body; + this._rawDoc = null; this._doc = {}; this._jsdoc = null; + this.path = (this._path.value || '').replace(/\/$/, ''); + this.method_name = (this.method.value || '').toLowerCase(); + this.body_params = []; + if (['post','put'].includes(this.method_name)) getReqBodyElems(this.body, this.body_params); + let url = this.path.replace(/:([^\/]*)Id/g, '{$1}').replace(/:([^\/]*)/g, '{$1}'); + this.url = url; + const tokens = url.split('/'); + const reduced = []; + for (let i = 0; i < tokens.length; i++) { + const t = tokens[i]; + if (t === 'api') continue; + if (i < tokens.length - 1 && tokens[i+1].startsWith('{')) continue; + reduced.push(t.replace(/[{}]/g, '')); + } + this.reduced_function_name = reduced.join('_'); + schema.used = true; + } + set doc(doc) { this._rawDoc = doc; this._jsdoc = cleanupJsdocs(doc); this._doc = this.parseDoc(); } + get doc() { return this._doc; } + parseDoc() { + if (!this._jsdoc) return {}; + const result = {}; + let currentTag = 'description'; + let current = ''; + const store = (tag, data) => { + if (data == null) return; const s = (data + '').replace(/\s+$/,''); if (!s) return; + if (tag === 'param') { + result.params = result.params || {}; + let nameDesc = s.trim(); + let paramType = null; + let name = nameDesc; let desc = ''; + const mType = nameDesc.match(/^\{([^}]+)\}\s*(.*)$/); + if (mType) { paramType = mType[1]; nameDesc = mType[2]; } + const sp = nameDesc.split(/\s+/, 2); name = sp[0]; desc = sp[1] || ''; + const optional = /^\[.*\]$/.test(name); if (optional) name = name.slice(1,-1); + result.params[name] = [paramType, optional, desc]; + if (name.endsWith('Id')) { const base = name.slice(0,-2); if (!result.params[base]) result.params[base] = [paramType, optional, desc]; } + return; + } + if (tag === 'tag') { (result.tag = result.tag || []).push(s); return; } + if (tag === 'return_type') { result[tag] = loadReturnTypeJsdocJson(s); return; } + result[tag] = s; + }; + for (const lineRaw of this._jsdoc) { + let line = lineRaw; + if (/^@/.test(line.trim())) { + const parts = line.trim().split(/\s+/, 2); + const tag = parts[0]; const rest = parts[1] || ''; + if (['@operation','@summary','@description','@param','@return_type','@tag'].includes(tag)) { + store(currentTag, current); + currentTag = tag.slice(1); current = ''; line = rest; + } + } + current += line + '\n'; + } + store(currentTag, current); + return result; + } + get summary() { return this._doc.summary ? this._doc.summary.replace(/\n/g,' ') : null; } + docParam(name) { const p = (this._doc.params||{})[name]; return p ? p : [null,null,null]; } + operationId() { return this._doc.operation || `${this.method_name}_${this.reduced_function_name}`; } + description() { return this._doc.description || null; } + returns() { return this._doc.return_type || null; } + tags() { const tags = []; if (this.schema.fields) tags.push(this.schema.name); if (this._doc.tag) tags.push(...this._doc.tag); return tags; } +} + +class SchemaProperty { + constructor(statement, schema, context) { + this.schema = schema; + this.statement = statement; + this.name = (statement.key.name || statement.key.value); + this.type = 'object'; this.blackbox = false; this.required = true; this.elements = []; + (statement.value.properties || []).forEach(p => { + try { + const key = p.key && (p.key.name || p.key.value); + if (key === 'type') { + if (p.value.type === 'Identifier') this.type = (p.value.name || '').toLowerCase(); + else if (p.value.type === 'ArrayExpression') { this.type = 'array'; this.elements = (p.value.elements||[]).map(e => (e.name||'object').toLowerCase()); } + } else if (key === 'blackbox') { this.blackbox = true; } + else if (key === 'optional' && p.value && p.value.value) { this.required = false; } + } catch(e) { /* ignore minor parse errors */ } + }); + this._doc = null; this._raw_doc = null; + } + set doc(jsdoc){ this._raw_doc = jsdoc; this._doc = cleanupJsdocs(jsdoc); } + get doc(){ return this._doc; } +} + +class Schemas { + constructor(context, statement, jsdocs, name) { + this.name = name || null; this._data = statement; this.fields = null; this.used = false; + if (statement) { + if (!this.name) this.name = statement.expression.callee.object.name; + const content = statement.expression.arguments[0].arguments[0]; + this.fields = (content.properties || []).map(p => new SchemaProperty(p, this, context)); + } + this._doc = null; this._raw_doc = null; + if (jsdocs) this.processJsdocs(jsdocs); + } + get doc(){ return this._doc ? this._doc.join(' ') : null; } + set doc(jsdoc){ this._raw_doc = jsdoc; this._doc = cleanupJsdocs(jsdoc); } + processJsdocs(jsdocs){ + if (!this._data) return; + const start = this._data.loc.start.line, end = this._data.loc.end.line; + for (const doc of jsdocs){ if (doc.loc.end.line + 1 === start) { this.doc = doc; break; } } + const inRange = jsdocs.filter(doc => doc.loc.start.line >= start && doc.loc.end.line <= end); + for (const f of (this.fields||[])) + for (let i=0;i { + const items = fs.readdirSync(dir, { withFileTypes: true }).sort((a,b)=>a.name.localeCompare(b.name)); + for (const it of items){ + const p = path.join(dir, it.name); + if (it.isDirectory()) walk(p); + else if (it.isFile()){ + const context = parseFile(p); if (!context) continue; const program = context.program; + let currentSchema = null; + const jsdocs = (program.comments||[]).filter(c => c.type === 'Block' && c.value.startsWith('*\n')); + for (const statement of program.body){ + try { + if (statement.type === 'ExpressionStatement' && statement.expression && statement.expression.callee && statement.expression.callee.property && statement.expression.callee.property.name === 'attachSchema' && statement.expression.arguments[0] && statement.expression.arguments[0].type === 'NewExpression' && statement.expression.arguments[0].callee.name === 'SimpleSchema'){ + const schema = new Schemas(context, statement, jsdocs); + currentSchema = schema.name; schemas[currentSchema] = schema; + } else if (statement.type === 'IfStatement' && statement.test && statement.test.type === 'MemberExpression' && statement.test.object && statement.test.object.name === 'Meteor' && statement.test.property && statement.test.property.name === 'isServer'){ + const conseq = statement.consequent && statement.consequent.body || []; + const data = conseq.filter(s => s.type === 'ExpressionStatement' && s.expression && s.expression.type === 'CallExpression' && s.expression.callee && s.expression.callee.object && s.expression.callee.object.name === 'JsonRoutes').map(s => s.expression.arguments); + if (data.length){ + if (!currentSchema){ currentSchema = path.basename(p); schemas[currentSchema] = new Schemas(context, null, null, currentSchema); } + const eps = data.map(d => new EntryPoint(schemas[currentSchema], d)); entryPoints.push(...eps); + let endOfPrev = -1; + for (const ep of eps){ + const op = ep.method; const prior = jsdocs.filter(j => j.loc.end.line + 1 <= op.loc.start.line && j.loc.start.line > endOfPrev); + if (prior.length) ep.doc = prior[prior.length - 1]; + endOfPrev = op.loc.end.line; + } + } + } + } catch(e){ /* ignore parse hiccups per file */ } + } + } + } + }; + walk(schemasDir); + return { schemas, entryPoints }; +} + +function printOpenapiReturn(obj, indent){ + const pad = ' '.repeat(indent); + if (Array.isArray(obj)){ + console.log(`${pad}type: array`); console.log(`${pad}items:`); printOpenapiReturn(obj[0], indent+2); return; + } + if (obj && typeof obj === 'object'){ + console.log(`${pad}type: object`); console.log(`${pad}properties:`); + for (const k of Object.keys(obj)){ console.log(`${pad} ${k}:`); printOpenapiReturn(obj[k], indent+4); } + return; + } + if (typeof obj === 'string') console.log(`${pad}type: ${obj}`); +} + +function generateOpenapi(schemas, entryPoints, version){ + console.log(`swagger: '2.0' +info: + title: Wekan REST API + version: ${version} + description: | + The REST API allows you to control and extend Wekan with ease. +schemes: + - http +securityDefinitions: + UserSecurity: + type: apiKey + in: header + name: Authorization +paths: + /users/login: + post: + operationId: login + summary: Login with REST API + consumes: + - application/json + - application/x-www-form-urlencoded + tags: + - Login + parameters: + - name: loginRequest + in: body + required: true + description: Login credentials + schema: + type: object + required: + - username + - password + properties: + username: + description: | + Your username + type: string + password: + description: | + Your password + type: string + format: password + responses: + 200: + description: |- + Successful authentication + schema: + type: object + required: + - id + - token + - tokenExpires + properties: + id: + type: string + description: User ID + token: + type: string + description: | + Authentication token + tokenExpires: + type: string + format: date-time + description: | + Token expiration date + 400: + description: | + Error in authentication + schema: + type: object + properties: + error: + type: string + reason: + type: string + default: + description: | + Error in authentication`); + + const methods = {}; + for (const ep of entryPoints){ (methods[ep.path] = methods[ep.path] || []).push(ep); } + const sorted = Object.keys(methods).sort(); + for (const pth of sorted){ + console.log(` ${methods[pth][0].url}:`); + for (const ep of methods[pth]){ + const parameters = pth.split('/').filter(t => t.startsWith(':')).map(t => t.endsWith('Id') ? t.slice(1,-2) : t.slice(1)); + console.log(` ${ep.method_name}:`); + console.log(` operationId: ${ep.operationId()}`); + const sum = ep.summary(); if (sum) console.log(` summary: ${sum}`); + const desc = ep.description(); if (desc){ console.log(` description: |`); desc.split('\n').forEach(l => console.log(` ${l.trim() ? l : ''}`)); } + const tags = ep.tags(); if (tags.length){ console.log(' tags:'); tags.forEach(t => console.log(` - ${t}`)); } + if (['post','put'].includes(ep.method_name)) console.log(` consumes:\n - multipart/form-data\n - application/json`); + if (parameters.length || ['post','put'].includes(ep.method_name)) console.log(' parameters:'); + if (['post','put'].includes(ep.method_name)){ + for (const f of ep.body_params){ + console.log(` - name: ${f}\n in: formData`); + const [ptype, optional, pdesc] = ep.docParam(f); + if (pdesc) console.log(` description: |\n ${pdesc}`); else console.log(` description: the ${f} value`); + console.log(` type: ${ptype || 'string'}`); + console.log(` ${optional ? 'required: false' : 'required: true'}`); + } + } + for (const p of parameters){ + console.log(` - name: ${p}\n in: path`); + const [ptype, optional, pdesc] = ep.docParam(p); + if (pdesc) console.log(` description: |\n ${pdesc}`); else console.log(` description: the ${p} value`); + console.log(` type: ${ptype || 'string'}`); + console.log(` ${optional ? 'required: false' : 'required: true'}`); + } + console.log(` produces:\n - application/json\n security:\n - UserSecurity: []\n responses:\n '200':\n description: |-\n 200 response`); + const ret = ep.returns(); + if (ret){ console.log(' schema:'); printOpenapiReturn(ret, 12); } + } + } + console.log('definitions:'); + for (const schema of Object.values(schemas)){ + if (!schema.used || !schema.fields) continue; + console.log(` ${schema.name}:`); + console.log(' type: object'); + if (schema.doc) console.log(` description: ${schema.doc}`); + console.log(' properties:'); + const props = schema.fields.filter(f => !f.name.includes('.')); + const req = []; + for (const prop of props){ + const name = prop.name; console.log(` ${name}:`); + if (prop.doc){ console.log(' description: |'); prop.doc.forEach(l => console.log(` ${l.trim() ? l : ''}`)); } + let ptype = prop.type; if (ptype === 'enum' || ptype === 'date') ptype = 'string'; + if (ptype !== 'object') console.log(` type: ${ptype}`); + if (prop.type === 'array'){ + console.log(' items:'); + for (const el of prop.elements){ if (el === 'object') console.log(` $ref: "#/definitions/${schema.name + name.charAt(0).toUpperCase() + name.slice(1)}"`); else console.log(` type: ${el}`); } + } else if (prop.type === 'object'){ + if (prop.blackbox) console.log(' type: object'); + else console.log(` $ref: "#/definitions/${schema.name + name.charAt(0).toUpperCase() + name.slice(1)}"`); + } + if (!prop.name.includes('.') && !prop.required) console.log(' x-nullable: true'); + if (prop.required) req.push(name); + } + if (req.length){ console.log(' required:'); req.forEach(f => console.log(` - ${f}`)); } + } +} + +function main(){ + const argv = process.argv.slice(2); + let version = 'git-master'; + let dir = path.resolve(__dirname, '../models'); + for (let i = 0; i < argv.length; i++){ + if (argv[i] === '--release' && argv[i+1]) { version = argv[i+1]; i++; continue; } + if (!argv[i].startsWith('--')) { dir = path.resolve(argv[i]); } + } + const { schemas, entryPoints } = parseSchemas(dir); + generateOpenapi(schemas, entryPoints, version); +} + +if (require.main === module) main(); + + diff --git a/package-lock.json b/package-lock.json index 560661b75..d0c858ffc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "wekan", - "version": "v8.01.0", + "version": "v8.02.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 67846ede4..8a729588c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wekan", - "version": "v8.01.0", + "version": "v8.02.0", "description": "Open-Source kanban", "private": true, "repository": { diff --git a/public/api/wekan.yml b/public/api/wekan.yml index 23ff7cbce..cbb1a23a6 100644 --- a/public/api/wekan.yml +++ b/public/api/wekan.yml @@ -1,7 +1,325 @@ +{ + type: "ConditionalExpression", + test: { + type: "LogicalExpression", + operator: "&&", + left: { + type: "Identifier", + name: "TAPi18n", + loc: {start: {line: 2217,column: 15},end: {line: 2217,column: 22}} + }, + right: { + type: "MemberExpression", + computed: False, + object: { + type: "Identifier", + name: "TAPi18n", + loc: {start: {line: 2217,column: 26},end: {line: 2217,column: 33}} + }, + property: { + type: "Identifier", + name: "i18n", + loc: {start: {line: 2217,column: 34},end: {line: 2217,column: 38}} + }, + loc: {start: {line: 2217,column: 26},end: {line: 2217,column: 38}} + }, + loc: {start: {line: 2217,column: 15},end: {line: 2217,column: 38}} + }, + consequent: { + type: "CallExpression", + callee: { + type: "MemberExpression", + computed: False, + object: { + type: "Identifier", + name: "TAPi18n", + loc: {start: {line: 2217,column: 41},end: {line: 2217,column: 48}} + }, + property: { + type: "Identifier", + name: "__", + loc: {start: {line: 2217,column: 49},end: {line: 2217,column: 51}} + }, + loc: {start: {line: 2217,column: 41},end: {line: 2217,column: 51}} + }, + arguments: [ + { + type: "Literal", + value: "default", + raw: "'default'", + loc: {start: {line: 2217,column: 52},end: {line: 2217,column: 61}} + } + ], + loc: {start: {line: 2217,column: 41},end: {line: 2217,column: 62}} + }, + alternate: { + type: "Literal", + value: "Default", + raw: "'Default'", + loc: {start: {line: 2217,column: 65},end: {line: 2217,column: 74}} + }, + loc: {start: {line: 2217,column: 15},end: {line: 2217,column: 74}} +} +{ + type: "ConditionalExpression", + test: { + type: "BinaryExpression", + operator: ">", + left: { + type: "MemberExpression", + computed: False, + object: { + type: "MemberExpression", + computed: False, + object: { + type: "MemberExpression", + computed: False, + object: { + type: "Identifier", + name: "req", + loc: {start: {line: 3587,column: 25},end: {line: 3587,column: 28}} + }, + property: { + type: "Identifier", + name: "body", + loc: {start: {line: 3587,column: 29},end: {line: 3587,column: 33}} + }, + loc: {start: {line: 3587,column: 25},end: {line: 3587,column: 33}} + }, + property: { + type: "Identifier", + name: "title", + loc: {start: {line: 3587,column: 34},end: {line: 3587,column: 39}} + }, + loc: {start: {line: 3587,column: 25},end: {line: 3587,column: 39}} + }, + property: { + type: "Identifier", + name: "length", + loc: {start: {line: 3587,column: 40},end: {line: 3587,column: 46}} + }, + loc: {start: {line: 3587,column: 25},end: {line: 3587,column: 46}} + }, + right: { + type: "Literal", + value: 1000, + raw: "1000", + loc: {start: {line: 3587,column: 49},end: {line: 3587,column: 53}} + }, + loc: {start: {line: 3587,column: 25},end: {line: 3587,column: 53}} + }, + consequent: { + type: "CallExpression", + callee: { + type: "MemberExpression", + computed: False, + object: { + type: "MemberExpression", + computed: False, + object: { + type: "MemberExpression", + computed: False, + object: { + type: "Identifier", + name: "req", + loc: {start: {line: 3587,column: 56},end: {line: 3587,column: 59}} + }, + property: { + type: "Identifier", + name: "body", + loc: {start: {line: 3587,column: 60},end: {line: 3587,column: 64}} + }, + loc: {start: {line: 3587,column: 56},end: {line: 3587,column: 64}} + }, + property: { + type: "Identifier", + name: "title", + loc: {start: {line: 3587,column: 65},end: {line: 3587,column: 70}} + }, + loc: {start: {line: 3587,column: 56},end: {line: 3587,column: 70}} + }, + property: { + type: "Identifier", + name: "substring", + loc: {start: {line: 3587,column: 71},end: {line: 3587,column: 80}} + }, + loc: {start: {line: 3587,column: 56},end: {line: 3587,column: 80}} + }, + arguments: [ + { + type: "Literal", + value: 0, + raw: "0", + loc: {start: {line: 3587,column: 81},end: {line: 3587,column: 82}} + }, + { + type: "Literal", + value: 1000, + raw: "1000", + loc: {start: {line: 3587,column: 84},end: {line: 3587,column: 88}} + } + ], + loc: {start: {line: 3587,column: 56},end: {line: 3587,column: 89}} + }, + alternate: { + type: "MemberExpression", + computed: False, + object: { + type: "MemberExpression", + computed: False, + object: { + type: "Identifier", + name: "req", + loc: {start: {line: 3587,column: 92},end: {line: 3587,column: 95}} + }, + property: { + type: "Identifier", + name: "body", + loc: {start: {line: 3587,column: 96},end: {line: 3587,column: 100}} + }, + loc: {start: {line: 3587,column: 92},end: {line: 3587,column: 100}} + }, + property: { + type: "Identifier", + name: "title", + loc: {start: {line: 3587,column: 101},end: {line: 3587,column: 106}} + }, + loc: {start: {line: 3587,column: 92},end: {line: 3587,column: 106}} + }, + loc: {start: {line: 3587,column: 25},end: {line: 3587,column: 106}} +} +{ + type: "ConditionalExpression", + test: { + type: "BinaryExpression", + operator: ">", + left: { + type: "MemberExpression", + computed: False, + object: { + type: "MemberExpression", + computed: False, + object: { + type: "MemberExpression", + computed: False, + object: { + type: "Identifier", + name: "req", + loc: {start: {line: 655,column: 25},end: {line: 655,column: 28}} + }, + property: { + type: "Identifier", + name: "body", + loc: {start: {line: 655,column: 29},end: {line: 655,column: 33}} + }, + loc: {start: {line: 655,column: 25},end: {line: 655,column: 33}} + }, + property: { + type: "Identifier", + name: "title", + loc: {start: {line: 655,column: 34},end: {line: 655,column: 39}} + }, + loc: {start: {line: 655,column: 25},end: {line: 655,column: 39}} + }, + property: { + type: "Identifier", + name: "length", + loc: {start: {line: 655,column: 40},end: {line: 655,column: 46}} + }, + loc: {start: {line: 655,column: 25},end: {line: 655,column: 46}} + }, + right: { + type: "Literal", + value: 1000, + raw: "1000", + loc: {start: {line: 655,column: 49},end: {line: 655,column: 53}} + }, + loc: {start: {line: 655,column: 25},end: {line: 655,column: 53}} + }, + consequent: { + type: "CallExpression", + callee: { + type: "MemberExpression", + computed: False, + object: { + type: "MemberExpression", + computed: False, + object: { + type: "MemberExpression", + computed: False, + object: { + type: "Identifier", + name: "req", + loc: {start: {line: 655,column: 56},end: {line: 655,column: 59}} + }, + property: { + type: "Identifier", + name: "body", + loc: {start: {line: 655,column: 60},end: {line: 655,column: 64}} + }, + loc: {start: {line: 655,column: 56},end: {line: 655,column: 64}} + }, + property: { + type: "Identifier", + name: "title", + loc: {start: {line: 655,column: 65},end: {line: 655,column: 70}} + }, + loc: {start: {line: 655,column: 56},end: {line: 655,column: 70}} + }, + property: { + type: "Identifier", + name: "substring", + loc: {start: {line: 655,column: 71},end: {line: 655,column: 80}} + }, + loc: {start: {line: 655,column: 56},end: {line: 655,column: 80}} + }, + arguments: [ + { + type: "Literal", + value: 0, + raw: "0", + loc: {start: {line: 655,column: 81},end: {line: 655,column: 82}} + }, + { + type: "Literal", + value: 1000, + raw: "1000", + loc: {start: {line: 655,column: 84},end: {line: 655,column: 88}} + } + ], + loc: {start: {line: 655,column: 56},end: {line: 655,column: 89}} + }, + alternate: { + type: "MemberExpression", + computed: False, + object: { + type: "MemberExpression", + computed: False, + object: { + type: "Identifier", + name: "req", + loc: {start: {line: 655,column: 92},end: {line: 655,column: 95}} + }, + property: { + type: "Identifier", + name: "body", + loc: {start: {line: 655,column: 96},end: {line: 655,column: 100}} + }, + loc: {start: {line: 655,column: 92},end: {line: 655,column: 100}} + }, + property: { + type: "Identifier", + name: "title", + loc: {start: {line: 655,column: 101},end: {line: 655,column: 106}} + }, + loc: {start: {line: 655,column: 92},end: {line: 655,column: 106}} + }, + loc: {start: {line: 655,column: 25},end: {line: 655,column: 106}} +} swagger: '2.0' info: title: Wekan REST API - version: v8.01 + version: v8.02 description: | The REST API allows you to control and extend Wekan with ease. @@ -2853,6 +3171,11 @@ definitions: The default board ID assigned to subtasks. type: string x-nullable: true + migrationVersion: + description: | + The migration version of the board structure. + New boards are created with the latest version and don't need migration. + type: number subtasksDefaultListId: description: | The default List ID assigned to subtasks. @@ -3041,6 +3364,7 @@ definitions: - color - allowsCardCounterList - allowsBoardMemberList + - migrationVersion - allowsSubtasks - allowsAttachments - allowsChecklists @@ -3821,8 +4145,9 @@ definitions: type: string swimlaneId: description: | - the swimlane associated to this list. Required for per-swimlane list titles + the swimlane associated to this list. Optional for backward compatibility type: string + x-nullable: true createdAt: description: | creation date @@ -3887,7 +4212,6 @@ definitions: - title - archived - boardId - - swimlaneId - createdAt - modifiedAt - type diff --git a/releases/rebuild-docs.sh b/releases/rebuild-docs.sh index 5121a66e1..0bc504765 100755 --- a/releases/rebuild-docs.sh +++ b/releases/rebuild-docs.sh @@ -1,4 +1,6 @@ -# Extract the OpenAPI specification. +#!/usr/bin/env bash +# Build API documentation using Node.js tooling only (Node 14.x compatible). +set -euo pipefail # 1) Check that there is only one parameter # of Wekan version number: @@ -10,26 +12,7 @@ if [ $# -ne 1 ] exit 1 fi -# 2) If esprima-python directory does not exist, -# install dependencies. - -if [ ! -d ~/python/esprima-python ]; then - sudo apt-get -y install python3-pip python3-swagger-spec-validator python3-wheel python3-setuptools - # Install older version of api2html that works with Node.js 14 - sudo npm install -g api2html@0.3.0 || sudo npm install -g swagger-ui-watcher - (mkdir -p ~/python && cd ~/python && git clone --depth 1 -b master https://github.com/Kronuz/esprima-python) - (cd ~/python/esprima-python && git fetch origin pull/20/head:delete_fix && git checkout delete_fix && sudo python3 setup.py install --record files.txt) - #(cd ~/python/esprima-python && git fetch origin pull/20/head:delete_fix && git checkout delete_fix && sudo pip3 install .) - # temporary fix until https://github.com/Kronuz/esprima-python/pull/20 gets merged - # a) Generating docs works on Kubuntu 21.10 with this, - # but generating Sandstorm WeKan package does not work - # https://github.com/wekan/wekan/issues/4280 - # https://github.com/sandstorm-io/sandstorm/issues/3600 - # sudo pip3 install . - # b) Generating docs Works on Linux Mint with this, - # and also generating Sandstorm WeKan package works: - # sudo python3 setup.py install --record files.txt -fi +# 2) No Python dependencies; use npm/npx exclusively # 2) Go to Wekan repo directory cd ~/repos/wekan @@ -39,10 +22,43 @@ if [ ! -d public/api ]; then mkdir -p public/api fi -# 4) Generate docs with api2html or fallback to swagger-ui-watcher -python3 ./openapi/generate_openapi.py --release v$1 > ./public/api/wekan.yml -if ! api2html -c ./public/logo-header.png -o ./public/api/wekan.html ./public/api/wekan.yml; then - swagger-ui-watcher ./public/api/wekan.yml -p 8080 +# 4) Locate or generate an OpenAPI spec (YAML or JSON) +SPEC_YML="./public/api/wekan.yml" +SPEC_JSON="./public/openapi.json" +SPEC_ALT_YML="./public/openapi.yml" + +if [ -s "$SPEC_YML" ]; then + SPEC="$SPEC_YML" +elif [ -s "$SPEC_JSON" ]; then + SPEC="$SPEC_JSON" +elif [ -s "$SPEC_ALT_YML" ]; then + SPEC="$SPEC_ALT_YML" +else + echo "No existing OpenAPI spec found. Generating from models with Node..." + mkdir -p ./public/api + node ./openapi/generate_openapi.js --release v$1 ./models > "$SPEC_YML" + SPEC="$SPEC_YML" +fi +chmod 644 "$SPEC" 2>/dev/null || true + +# Build static HTML docs (no global installs) +# 1) Prefer Redocly CLI +if npx --yes @redocly/cli@latest build-docs "$SPEC" -o ./public/api/wekan.html; then + : +else + # 2) Fallback to redoc-cli + if npx --yes redoc-cli@latest bundle "$SPEC" -o ./public/api/wekan.html; then + : + else + # 3) Fallback to api2html + if npx --yes api2html@0.3.0 -c ./public/logo-header.png -o ./public/api/wekan.html "$SPEC"; then + : + else + echo "All HTML generators failed. You can preview locally with:" >&2 + echo " npx --yes @redocly/cli@latest preview-docs $SPEC" >&2 + exit 1 + fi + fi fi # Copy docs to bundle diff --git a/sandstorm-pkgdef.capnp b/sandstorm-pkgdef.capnp index 8de01300a..1fa6af6ab 100644 --- a/sandstorm-pkgdef.capnp +++ b/sandstorm-pkgdef.capnp @@ -22,10 +22,10 @@ const pkgdef :Spk.PackageDefinition = ( appTitle = (defaultText = "Wekan"), # The name of the app as it is displayed to the user. - appVersion = 801, + appVersion = 802, # Increment this for every release. - appMarketingVersion = (defaultText = "8.01.0~2025-10-11"), + appMarketingVersion = (defaultText = "8.02.0~2025-10-14"), # Human-readable presentation of the app version. minUpgradableAppVersion = 0, diff --git a/snapcraft.yaml b/snapcraft.yaml index f98d7c917..1a3dfaac9 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -1,5 +1,5 @@ name: wekan -version: '8.01' +version: '8.02' base: core24 summary: Open Source kanban description: | @@ -203,9 +203,9 @@ parts: # Cleanup mkdir .build cd .build - wget https://github.com/wekan/wekan/releases/download/v8.01/wekan-8.01-amd64.zip - unzip wekan-8.01-amd64.zip - rm wekan-8.01-amd64.zip + wget https://github.com/wekan/wekan/releases/download/v8.02/wekan-8.02-amd64.zip + unzip wekan-8.02-amd64.zip + rm wekan-8.02-amd64.zip cd .. ##cd .build/bundle ##find . -type d -name '*-garbage*' | xargs rm -rf