mirror of
https://github.com/wekan/wekan.git
synced 2026-01-11 03:58:50 +01:00
409 lines
14 KiB
Markdown
409 lines
14 KiB
Markdown
# Wekan Data Persistence Architecture - 2025-12-23
|
|
|
|
**Status**: ✅ Latest Current
|
|
**Updated**: 2025-12-23
|
|
**Scope**: All data persistence related to swimlanes, lists, cards, checklists, checklistItems positioning and user preferences
|
|
|
|
---
|
|
|
|
## Executive Summary
|
|
|
|
Wekan's data persistence architecture distinguishes between:
|
|
- **Board-Level Data**: Shared across all users on a board (positions, widths, heights, order)
|
|
- **Per-User Data**: Private to each user, not visible to others (collapse state, label visibility)
|
|
|
|
This document defines the authoritative source of truth for all persistence decisions.
|
|
|
|
---
|
|
|
|
## Data Classification Matrix
|
|
|
|
### ✅ PER-BOARD LEVEL (Shared - Stored in MongoDB Documents)
|
|
|
|
| Entity | Property | Storage | Format | Scope |
|
|
|--------|----------|---------|--------|-------|
|
|
| **Swimlane** | Title | MongoDB | String | Board |
|
|
| **Swimlane** | Color | MongoDB | String (ALLOWED_COLORS) | Board |
|
|
| **Swimlane** | Background | MongoDB | Object {color} | Board |
|
|
| **Swimlane** | Height | MongoDB | Number (-1=auto, 50-2000) | Board |
|
|
| **Swimlane** | Position/Sort | MongoDB | Number (decimal) | Board |
|
|
| **List** | Title | MongoDB | String | Board |
|
|
| **List** | Color | MongoDB | String (ALLOWED_COLORS) | Board |
|
|
| **List** | Background | MongoDB | Object {color} | Board |
|
|
| **List** | Width | MongoDB | Number (100-1000) | Board |
|
|
| **List** | Position/Sort | MongoDB | Number (decimal) | Board |
|
|
| **List** | WIP Limit | MongoDB | Object {enabled, value, soft} | Board |
|
|
| **List** | Starred | MongoDB | Boolean | Board |
|
|
| **Card** | Title | MongoDB | String | Board |
|
|
| **Card** | Color | MongoDB | String (ALLOWED_COLORS) | Board |
|
|
| **Card** | Background | MongoDB | Object {color} | Board |
|
|
| **Card** | Description | MongoDB | String | Board |
|
|
| **Card** | Position/Sort | MongoDB | Number (decimal) | Board |
|
|
| **Card** | ListId | MongoDB | String | Board |
|
|
| **Card** | SwimlaneId | MongoDB | String | Board |
|
|
| **Checklist** | Title | MongoDB | String | Board |
|
|
| **Checklist** | Position/Sort | MongoDB | Number (decimal) | Board |
|
|
| **Checklist** | hideCheckedItems | MongoDB | Boolean | Board |
|
|
| **Checklist** | hideAllItems | MongoDB | Boolean | Board |
|
|
| **ChecklistItem** | Title | MongoDB | String | Board |
|
|
| **ChecklistItem** | isFinished | MongoDB | Boolean | Board |
|
|
| **ChecklistItem** | Position/Sort | MongoDB | Number (decimal) | Board |
|
|
|
|
### 🔒 PER-USER ONLY (Private - User Profile or localStorage)
|
|
|
|
| Entity | Property | Storage | Format | Users |
|
|
|--------|----------|---------|--------|-------|
|
|
| **User** | Collapsed Swimlanes | User Profile / Cookie | Object {boardId: {swimlaneId: boolean}} | Single |
|
|
| **User** | Collapsed Lists | User Profile / Cookie | Object {boardId: {listId: boolean}} | Single |
|
|
| **User** | Hide Minicard Label Text | User Profile / localStorage | Object {boardId: boolean} | Single |
|
|
| **User** | Collapse Card Details View | Cookie | Boolean | Single |
|
|
|
|
---
|
|
|
|
## Implementation Details
|
|
|
|
### 1. Swimlanes Schema (swimlanes.js)
|
|
|
|
```javascript
|
|
Swimlanes.attachSchema(
|
|
new SimpleSchema({
|
|
title: { type: String }, // ✅ Per-board
|
|
color: { type: String, optional: true }, // ✅ Per-board (ALLOWED_COLORS)
|
|
// background: { ...color properties... } // ✅ Per-board (for future use)
|
|
height: { // ✅ Per-board (NEW)
|
|
type: Number,
|
|
optional: true,
|
|
defaultValue: -1, // -1 means auto-height
|
|
custom() {
|
|
const h = this.value;
|
|
if (h !== -1 && (h < 50 || h > 2000)) {
|
|
return 'heightOutOfRange';
|
|
}
|
|
},
|
|
},
|
|
sort: { type: Number, decimal: true, optional: true }, // ✅ Per-board
|
|
boardId: { type: String }, // ✅ Per-board
|
|
archived: { type: Boolean }, // ✅ Per-board
|
|
// NOTE: Collapse state is per-user only, stored in:
|
|
// - User profile: profile.collapsedSwimlanes[boardId][swimlaneId] = boolean
|
|
// - Non-logged-in: Cookie 'wekan-collapsed-swimlanes'
|
|
})
|
|
);
|
|
```
|
|
|
|
### 2. Lists Schema (lists.js)
|
|
|
|
```javascript
|
|
Lists.attachSchema(
|
|
new SimpleSchema({
|
|
title: { type: String }, // ✅ Per-board
|
|
color: { type: String, optional: true }, // ✅ Per-board (ALLOWED_COLORS)
|
|
// background: { ...color properties... } // ✅ Per-board (for future use)
|
|
width: { // ✅ Per-board (NEW)
|
|
type: Number,
|
|
optional: true,
|
|
defaultValue: 272, // default width in pixels
|
|
custom() {
|
|
const w = this.value;
|
|
if (w < 100 || w > 1000) {
|
|
return 'widthOutOfRange';
|
|
}
|
|
},
|
|
},
|
|
sort: { type: Number, decimal: true, optional: true }, // ✅ Per-board
|
|
swimlaneId: { type: String, optional: true }, // ✅ Per-board
|
|
boardId: { type: String }, // ✅ Per-board
|
|
archived: { type: Boolean }, // ✅ Per-board
|
|
wipLimit: { type: Object, optional: true }, // ✅ Per-board
|
|
starred: { type: Boolean, optional: true }, // ✅ Per-board
|
|
// NOTE: Collapse state is per-user only, stored in:
|
|
// - User profile: profile.collapsedLists[boardId][listId] = boolean
|
|
// - Non-logged-in: Cookie 'wekan-collapsed-lists'
|
|
})
|
|
);
|
|
```
|
|
|
|
### 3. Cards Schema (cards.js)
|
|
|
|
```javascript
|
|
Cards.attachSchema(
|
|
new SimpleSchema({
|
|
title: { type: String, optional: true }, // ✅ Per-board
|
|
color: { type: String, optional: true }, // ✅ Per-board (ALLOWED_COLORS)
|
|
// background: { ...color properties... } // ✅ Per-board (for future use)
|
|
description: { type: String, optional: true }, // ✅ Per-board
|
|
sort: { type: Number, decimal: true, optional: true }, // ✅ Per-board
|
|
swimlaneId: { type: String }, // ✅ Per-board (REQUIRED)
|
|
listId: { type: String, optional: true }, // ✅ Per-board
|
|
boardId: { type: String, optional: true }, // ✅ Per-board
|
|
archived: { type: Boolean }, // ✅ Per-board
|
|
// ... other fields are all per-board
|
|
})
|
|
);
|
|
```
|
|
|
|
### 4. Checklists Schema (checklists.js)
|
|
|
|
```javascript
|
|
Checklists.attachSchema(
|
|
new SimpleSchema({
|
|
title: { type: String }, // ✅ Per-board
|
|
sort: { type: Number, decimal: true }, // ✅ Per-board
|
|
hideCheckedChecklistItems: { type: Boolean, optional: true }, // ✅ Per-board
|
|
hideAllChecklistItems: { type: Boolean, optional: true }, // ✅ Per-board
|
|
cardId: { type: String }, // ✅ Per-board
|
|
})
|
|
);
|
|
```
|
|
|
|
### 5. ChecklistItems Schema (checklistItems.js)
|
|
|
|
```javascript
|
|
ChecklistItems.attachSchema(
|
|
new SimpleSchema({
|
|
title: { type: String }, // ✅ Per-board
|
|
sort: { type: Number, decimal: true }, // ✅ Per-board
|
|
isFinished: { type: Boolean }, // ✅ Per-board
|
|
checklistId: { type: String }, // ✅ Per-board
|
|
cardId: { type: String }, // ✅ Per-board
|
|
})
|
|
);
|
|
```
|
|
|
|
### 6. User Schema - Per-User Data (users.js)
|
|
|
|
```javascript
|
|
// User.profile structure for per-user data
|
|
user.profile = {
|
|
// Collapse states - per-user, per-board
|
|
collapsedSwimlanes: {
|
|
'boardId123': {
|
|
'swimlaneId456': true, // swimlane is collapsed for this user
|
|
'swimlaneId789': false
|
|
},
|
|
'boardId999': { ... }
|
|
},
|
|
|
|
// Collapse states - per-user, per-board
|
|
collapsedLists: {
|
|
'boardId123': {
|
|
'listId456': true, // list is collapsed for this user
|
|
'listId789': false
|
|
},
|
|
'boardId999': { ... }
|
|
},
|
|
|
|
// Label visibility - per-user, per-board
|
|
hideMiniCardLabelText: {
|
|
'boardId123': true, // hide minicard labels on this board
|
|
'boardId999': false
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Client-Side Storage (Non-Logged-In Users)
|
|
|
|
For users not logged in, collapse state is persisted via cookies (localStorage alternative):
|
|
|
|
```javascript
|
|
// Cookie: wekan-collapsed-swimlanes
|
|
{
|
|
'boardId123': {
|
|
'swimlaneId456': true,
|
|
'swimlaneId789': false
|
|
}
|
|
}
|
|
|
|
// Cookie: wekan-collapsed-lists
|
|
{
|
|
'boardId123': {
|
|
'listId456': true,
|
|
'listId789': false
|
|
}
|
|
}
|
|
|
|
// Cookie: wekan-card-collapsed
|
|
{
|
|
'state': false // is card details view collapsed
|
|
}
|
|
|
|
// localStorage: wekan-hide-minicard-label-{boardId}
|
|
true or false
|
|
```
|
|
|
|
---
|
|
|
|
## Data Flow
|
|
|
|
### ✅ Board-Level Data Flow (Swimlane Height Example)
|
|
|
|
```
|
|
1. User resizes swimlane in UI
|
|
2. Client calls: Swimlanes.update(swimlaneId, { $set: { height: 300 } })
|
|
3. MongoDB receives update
|
|
4. Schema validation: height must be -1 or 50-2000
|
|
5. Update stored in swimlanes collection: { _id, title, height: 300, ... }
|
|
6. Update reflected in Swimlanes collection reactive
|
|
7. All users viewing board see updated height
|
|
8. Persists across page reloads
|
|
9. Persists across browser restarts
|
|
```
|
|
|
|
### ✅ Per-User Data Flow (Collapse State Example)
|
|
|
|
```
|
|
1. User collapses swimlane in UI
|
|
2. Client detects LOGGED-IN or NOT-LOGGED-IN
|
|
3. If LOGGED-IN:
|
|
a. Client calls: Meteor.call('setCollapsedSwimlane', boardId, swimlaneId, true)
|
|
b. Server updates user profile: { profile: { collapsedSwimlanes: { ... } } }
|
|
c. Stored in users collection
|
|
4. If NOT-LOGGED-IN:
|
|
a. Client writes to cookie: wekan-collapsed-swimlanes
|
|
b. Stored in browser cookies
|
|
5. On next page load:
|
|
a. Client reads from profile (logged-in) or cookie (not logged-in)
|
|
b. UI restored to saved state
|
|
6. Collapse state NOT visible to other users
|
|
```
|
|
|
|
---
|
|
|
|
## Validation Rules
|
|
|
|
### Swimlane Height Validation
|
|
- **Allowed Values**: -1 (auto) or 50-2000 pixels
|
|
- **Default**: -1 (auto)
|
|
- **Trigger**: On insert/update
|
|
- **Action**: Reject if invalid
|
|
|
|
### List Width Validation
|
|
- **Allowed Values**: 100-1000 pixels
|
|
- **Default**: 272 pixels
|
|
- **Trigger**: On insert/update
|
|
- **Action**: Reject if invalid
|
|
|
|
### Collapse State Validation
|
|
- **Allowed Values**: true or false
|
|
- **Storage**: Only boolean values allowed
|
|
- **Trigger**: On read/write to profile
|
|
- **Action**: Remove if corrupted
|
|
|
|
---
|
|
|
|
## Migration Strategy
|
|
|
|
### For Existing Installations
|
|
|
|
1. **Add new fields to schemas**
|
|
- `Swimlanes.height` (default: -1)
|
|
- `Lists.width` (default: 272)
|
|
|
|
2. **Populate existing data**
|
|
- For swimlanes without height: set to -1 (auto)
|
|
- For lists without width: set to 272 (default)
|
|
|
|
3. **Remove per-user storage if present**
|
|
- Check user.profile.swimlaneHeights → migrate to swimlane.height
|
|
- Check user.profile.listWidths → migrate to list.width
|
|
- Remove old fields from user profile
|
|
|
|
4. **Validation migration**
|
|
- Ensure all swimlaneIds are valid (no orphaned data)
|
|
- Ensure all widths/heights are in valid range
|
|
- Clean corrupted per-user data
|
|
|
|
---
|
|
|
|
## Security Implications
|
|
|
|
### Per-User Data (🔒 Private)
|
|
- Collapse state is per-user → User A's collapse setting doesn't affect User B's view
|
|
- Hide label setting is per-user → User A's label visibility doesn't affect User B
|
|
- Stored in user profile → Only accessible to that user
|
|
- Cookies for non-logged-in → Stored locally, not transmitted
|
|
|
|
### Per-Board Data (✅ Shared)
|
|
- Heights/widths are shared → All users see same swimlane/list sizes
|
|
- Positions are shared → All users see same card order
|
|
- Colors are shared → All users see same visual styling
|
|
- Stored in MongoDB → All users can query and receive updates
|
|
|
|
### No Cross-User Leakage
|
|
- User A's preferences never stored in User B's profile
|
|
- User A's preferences never affect User B's view
|
|
- Each user has isolated per-user data space
|
|
|
|
---
|
|
|
|
## Testing Checklist
|
|
|
|
### Per-Board Data Tests
|
|
- [ ] Resize swimlane height → all users see change
|
|
- [ ] Resize list width → all users see change
|
|
- [ ] Move card between lists → all users see change
|
|
- [ ] Change card color → all users see change
|
|
- [ ] Reload page → changes persist
|
|
- [ ] Different browser → changes persist
|
|
|
|
### Per-User Data Tests
|
|
- [ ] User A collapses swimlane → User B sees it expanded
|
|
- [ ] User A hides labels → User B sees labels
|
|
- [ ] User A scrolls away → User B can collapse same swimlane
|
|
- [ ] Logout → cookies maintain collapse state
|
|
- [ ] Login as different user → previous collapse state not visible
|
|
- [ ] Reload page → collapse state restored for user
|
|
|
|
### Validation Tests
|
|
- [ ] Set swimlane height = 25 → rejected (< 50)
|
|
- [ ] Set swimlane height = 3000 → rejected (> 2000)
|
|
- [ ] Set list width = 50 → rejected (< 100)
|
|
- [ ] Set list width = 2000 → rejected (> 1000)
|
|
- [ ] Corrupt localStorage height → cleaned on startup
|
|
- [ ] Corrupt user profile height → cleaned on startup
|
|
|
|
---
|
|
|
|
## Related Files
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| [models/swimlanes.js](../../../models/swimlanes.js) | Swimlane model with height field |
|
|
| [models/lists.js](../../../models/lists.js) | List model with width field |
|
|
| [models/cards.js](../../../models/cards.js) | Card model with position tracking |
|
|
| [models/checklists.js](../../../models/checklists.js) | Checklist model |
|
|
| [models/checklistItems.js](../../../models/checklistItems.js) | ChecklistItem model |
|
|
| [models/users.js](../../../models/users.js) | User model with per-user settings |
|
|
|
|
---
|
|
|
|
## Glossary
|
|
|
|
| Term | Definition |
|
|
|------|-----------|
|
|
| **Per-Board** | Stored in swimlane/list/card document, visible to all users |
|
|
| **Per-User** | Stored in user profile/cookie, visible only to that user |
|
|
| **Sort** | Decimal number determining visual order of entity |
|
|
| **Height** | Pixel measurement of swimlane vertical size |
|
|
| **Width** | Pixel measurement of list horizontal size |
|
|
| **Collapse** | Hiding swimlane/list/card from view (per-user preference) |
|
|
| **Position** | Combination of swimlaneId/listId and sort value |
|
|
|
|
---
|
|
|
|
## Change Log
|
|
|
|
| Date | Change | Impact |
|
|
|------|--------|--------|
|
|
| 2025-12-23 | Created comprehensive architecture document | Documentation |
|
|
| 2025-12-23 | Added height field to Swimlanes | Per-board storage |
|
|
| 2025-12-23 | Added width field to Lists | Per-board storage |
|
|
| 2025-12-23 | Defined per-user data as collapse + label visibility | Architecture |
|
|
|
|
---
|
|
|
|
**Status**: ✅ Complete and Current
|
|
**Next Review**: Upon next architectural change
|
|
|