Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0093c4aec2 | ||
|
|
0563521887 | ||
|
|
9ade0fa0ca | ||
|
|
3ec934a54c | ||
|
|
39fd491d24 | ||
|
|
381fef8502 | ||
|
|
c4cc2712ee | ||
|
|
152d07f21e | ||
|
|
49ef8ae82f | ||
|
|
261a6d4d40 | ||
|
|
0b87f3e4ed | ||
|
|
7f8a764131 | ||
|
|
9a9aff0880 | ||
|
|
f188dde6e4 | ||
|
|
4566794f44 | ||
|
|
c727142689 | ||
|
|
4420c72965 | ||
|
|
e55528e69f | ||
|
|
e86aa5e535 | ||
|
|
59f95d8174 | ||
|
|
159777818d | ||
|
|
00ac23160e | ||
|
|
46f9b4434e | ||
|
|
d0cb43435b | ||
|
|
34bc152817 | ||
|
|
fc8c5976f8 | ||
|
|
259d21c968 | ||
|
|
21a2cffcc0 | ||
|
|
865e0b3301 | ||
|
|
7fad4b5eec | ||
|
|
3a5b5a68e4 | ||
|
|
2773255b38 | ||
|
|
85ef3dc38b | ||
|
|
87e3f59ee1 | ||
|
|
80663f7d39 | ||
|
|
ab4ddb9f8b | ||
|
|
6969e79029 | ||
|
|
448279d98c | ||
|
|
7e224301cd | ||
|
|
df97937948 | ||
|
|
ed8b406b7d | ||
|
|
e87d31aded | ||
|
|
964a4fe0f9 | ||
|
|
e07d28d6b1 | ||
|
|
ab26148a67 | ||
|
|
fd1c473cb6 | ||
|
|
56e6225dd1 | ||
|
|
bdc6562116 | ||
|
|
9312783793 | ||
|
|
b78ada0865 | ||
|
|
be2f77dbf1 | ||
|
|
2ca23e276f | ||
|
|
f3bcd78e49 | ||
|
|
4c3d1a118c | ||
|
|
4b6e87c979 | ||
|
|
ed31f24b23 |
@@ -0,0 +1,38 @@
|
||||
{"_e":"root:init","_t":1787490823136,"format":"v0-jsonl","version":"57.0.12"}
|
||||
{"_e":"env:mode","_t":1787490823267,"nodeEnv":"production","babelEnv":"production","mode":"production"}
|
||||
{"_e":"env:load","_t":1787490823269,"mode":"production","files":[],"env":{}}
|
||||
{"_e":"devserver:start","_t":1787490823283,"mode":"production","web":true,"baseUrl":"","asyncRoutes":false,"routerRoot":"../../../app","serverComponents":false,"serverActions":false,"serverRendering":false,"apiRoutes":false,"exporting":true}
|
||||
{"_e":"metro:config","_t":1787490823513,"serverRoot":"../../..","projectRoot":"../../..","exporting":true,"flags":{"autolinkingModuleResolution":false,"serverActions":false,"serverComponents":false,"reactCompiler":false,"optimizeGraph":false,"treeshaking":false,"logbox":false}}
|
||||
{"_e":"metro:instantiate","_t":1787490823624,"atlas":false,"workers":10,"host":null,"port":null}
|
||||
{"_e":"metro:bundling:started","_t":1787490824135,"id":"1","platform":"web","environment":null,"entry":"node_modules/expo-router/entry.js"}
|
||||
{"_e":"root:init","_t":1787490834893,"format":"v0-jsonl","version":"57.0.12"}
|
||||
{"_e":"env:mode","_t":1787490834983,"nodeEnv":"production","babelEnv":"production","mode":"production"}
|
||||
{"_e":"env:load","_t":1787490834985,"mode":"production","files":[],"env":{}}
|
||||
{"_e":"devserver:start","_t":1787490834998,"mode":"production","web":true,"baseUrl":"","asyncRoutes":false,"routerRoot":"../../../app","serverComponents":false,"serverActions":false,"serverRendering":false,"apiRoutes":false,"exporting":true}
|
||||
{"_e":"metro:config","_t":1787490835165,"serverRoot":"../../..","projectRoot":"../../..","exporting":true,"flags":{"autolinkingModuleResolution":false,"serverActions":false,"serverComponents":false,"reactCompiler":false,"optimizeGraph":false,"treeshaking":false,"logbox":false}}
|
||||
{"_e":"metro:instantiate","_t":1787490835236,"atlas":false,"workers":10,"host":null,"port":null}
|
||||
{"_e":"metro:bundling:started","_t":1787490835782,"id":"1","platform":"web","environment":null,"entry":"node_modules/expo-router/entry.js"}
|
||||
{"_e":"root:init","_t":1787490897618,"format":"v0-jsonl","version":"57.0.12"}
|
||||
{"_e":"env:mode","_t":1787490897716,"nodeEnv":"production","babelEnv":"production","mode":"production"}
|
||||
{"_e":"env:load","_t":1787490897718,"mode":"production","files":[],"env":{}}
|
||||
{"_e":"devserver:start","_t":1787490897732,"mode":"production","web":true,"baseUrl":"","asyncRoutes":false,"routerRoot":"../../../app","serverComponents":false,"serverActions":false,"serverRendering":false,"apiRoutes":false,"exporting":true}
|
||||
{"_e":"metro:config","_t":1787490897904,"serverRoot":"../../..","projectRoot":"../../..","exporting":true,"flags":{"autolinkingModuleResolution":false,"serverActions":false,"serverComponents":false,"reactCompiler":false,"optimizeGraph":false,"treeshaking":false,"logbox":false}}
|
||||
{"_e":"metro:instantiate","_t":1787490897978,"atlas":false,"workers":10,"host":null,"port":null}
|
||||
{"_e":"metro:bundling:started","_t":1787490898645,"id":"1","platform":"web","environment":null,"entry":"node_modules/expo-router/entry.js"}
|
||||
{"_e":"metro:bundling:done","_t":1787490904477,"id":"1","total":743,"ms":5831.675237}
|
||||
{"_e":"root:init","_t":1787490907952,"format":"v0-jsonl","version":"57.0.12"}
|
||||
{"_e":"env:mode","_t":1787490908055,"nodeEnv":"production","babelEnv":"production","mode":"production"}
|
||||
{"_e":"env:load","_t":1787490908056,"mode":"production","files":[],"env":{}}
|
||||
{"_e":"devserver:start","_t":1787490908069,"mode":"production","web":true,"baseUrl":"","asyncRoutes":false,"routerRoot":"../../../app","serverComponents":false,"serverActions":false,"serverRendering":false,"apiRoutes":false,"exporting":true}
|
||||
{"_e":"metro:config","_t":1787490908244,"serverRoot":"../../..","projectRoot":"../../..","exporting":true,"flags":{"autolinkingModuleResolution":false,"serverActions":false,"serverComponents":false,"reactCompiler":false,"optimizeGraph":false,"treeshaking":false,"logbox":false}}
|
||||
{"_e":"metro:instantiate","_t":1787490908323,"atlas":false,"workers":10,"host":null,"port":null}
|
||||
{"_e":"metro:bundling:started","_t":1787490908955,"id":"1","platform":"web","environment":null,"entry":"node_modules/expo-router/entry.js"}
|
||||
{"_e":"metro:bundling:done","_t":1787490909421,"id":"1","total":731,"ms":465.745099}
|
||||
{"_e":"root:init","_t":1787490971753,"format":"v0-jsonl","version":"57.0.12"}
|
||||
{"_e":"env:mode","_t":1787490972145,"nodeEnv":"production","babelEnv":"production","mode":"production"}
|
||||
{"_e":"env:load","_t":1787490972146,"mode":"production","files":[],"env":{}}
|
||||
{"_e":"devserver:start","_t":1787490972158,"mode":"production","web":true,"baseUrl":"","asyncRoutes":false,"routerRoot":"../../../app","serverComponents":false,"serverActions":false,"serverRendering":false,"apiRoutes":false,"exporting":true}
|
||||
{"_e":"metro:config","_t":1787490972351,"serverRoot":"../../..","projectRoot":"../../..","exporting":true,"flags":{"autolinkingModuleResolution":false,"serverActions":false,"serverComponents":false,"reactCompiler":false,"optimizeGraph":false,"treeshaking":false,"logbox":false}}
|
||||
{"_e":"metro:instantiate","_t":1787490972454,"atlas":false,"workers":10,"host":null,"port":null}
|
||||
{"_e":"metro:bundling:started","_t":1787490973074,"id":"1","platform":"web","environment":null,"entry":"node_modules/expo-router/entry.js"}
|
||||
{"_e":"metro:bundling:done","_t":1787490973517,"id":"1","total":743,"ms":441.956684}
|
||||
@@ -29,16 +29,25 @@ jobs:
|
||||
|
||||
- name: Install Android SDK
|
||||
run: |
|
||||
mkdir -p "$ANDROID_HOME/cmdline-tools"
|
||||
wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O /tmp/cmdtools.zip
|
||||
unzip -q /tmp/cmdtools.zip -d "$ANDROID_HOME/cmdline-tools"
|
||||
mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest"
|
||||
yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses > /dev/null
|
||||
if [ -x "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" ]; then
|
||||
echo "Android SDK already present in cache"
|
||||
else
|
||||
rm -rf "$ANDROID_HOME/cmdline-tools"
|
||||
mkdir -p "$ANDROID_HOME/cmdline-tools"
|
||||
wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O /tmp/cmdtools.zip
|
||||
unzip -q /tmp/cmdtools.zip -d "$ANDROID_HOME/cmdline-tools"
|
||||
mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest"
|
||||
fi
|
||||
printf 'y\n' | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses > /dev/null
|
||||
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" \
|
||||
"platform-tools" \
|
||||
"platforms;android-35" \
|
||||
"platforms;android-36" \
|
||||
"build-tools;35.0.0" \
|
||||
"build-tools;36.0.0" \
|
||||
"ndk;27.1.12297006"
|
||||
mkdir -p carry-your-live/android/app
|
||||
echo "sdk.dir=$ANDROID_HOME" > carry-your-live/android/app/local.properties
|
||||
|
||||
- name: Install JS dependencies
|
||||
working-directory: carry-your-live
|
||||
@@ -46,6 +55,8 @@ jobs:
|
||||
|
||||
- name: Generate debug keystore (signs the release APK)
|
||||
run: |
|
||||
mkdir -p carry-your-live/android/app
|
||||
rm -f carry-your-live/android/app/debug.keystore
|
||||
keytool -genkeypair -v \
|
||||
-keystore carry-your-live/android/app/debug.keystore \
|
||||
-alias androiddebugkey -storepass android -keypass android \
|
||||
@@ -53,11 +64,12 @@ jobs:
|
||||
-dname "CN=Android Debug,O=Android,C=US"
|
||||
|
||||
- name: Build release APK
|
||||
working-directory: carry-your-live/android
|
||||
run: ./gradlew assembleRelease
|
||||
run: |
|
||||
cd carry-your-live/android
|
||||
./gradlew assembleRelease
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: carry-your-live-release
|
||||
path: carry-your-live/android/app/build/outputs/apk/release/*.apk
|
||||
path: carry-your-live/android/app/build/outputs/release/app-release.apk
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Build Desktop (deb)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install JS dependencies
|
||||
working-directory: carry-your-live
|
||||
run: npm ci
|
||||
|
||||
- name: Export web bundle
|
||||
working-directory: carry-your-live
|
||||
run: npx expo export --platform web
|
||||
|
||||
- name: Build deb package
|
||||
working-directory: carry-your-live
|
||||
run: npx electron-builder --linux deb --config electron-builder.config.js
|
||||
|
||||
- name: Upload deb
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: carry-your-live-desktop-deb
|
||||
path: carry-your-live/desktop-build/*.deb
|
||||
@@ -0,0 +1,56 @@
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Compute next version
|
||||
id: version
|
||||
run: |
|
||||
latest=$(git tag --list 'v[0-9]*' --sort=-v:refname | head -n 1)
|
||||
if [ -z "$latest" ]; then
|
||||
next="v1.0.0"
|
||||
else
|
||||
latest=${latest#v}
|
||||
IFS='.' read -r major minor patch <<< "$latest"
|
||||
next="v${major}.${minor}.$((patch + 1))"
|
||||
fi
|
||||
echo "next=${next}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build changelog
|
||||
id: changelog
|
||||
run: |
|
||||
prev_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
||||
if [ -z "$prev_tag" ]; then
|
||||
log=$(git log --oneline --no-merges main | head -n 40)
|
||||
else
|
||||
log=$(git log --oneline --no-merges "$prev_tag"..main | head -n 40)
|
||||
fi
|
||||
{
|
||||
echo "body<<EOF"
|
||||
echo "## What's new in this release"
|
||||
echo ""
|
||||
echo "$log" | sed 's/^/- /'
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ steps.version.outputs.next }}
|
||||
name: ${{ steps.version.outputs.next }}
|
||||
body: ${{ steps.changelog.outputs.body }}
|
||||
generate_release_notes: false
|
||||
@@ -1 +1,2 @@
|
||||
node_modules
|
||||
node_modules/
|
||||
dist/
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Architekt Agent
|
||||
|
||||
## Role
|
||||
System architecture design, technical strategy, and scalability planning.
|
||||
|
||||
## Responsibilities
|
||||
- High-level system design
|
||||
- Technology stack selection
|
||||
- Performance optimization
|
||||
- Architecture documentation
|
||||
- Trade-off analysis
|
||||
|
||||
## Key Metrics
|
||||
- System scalability
|
||||
- Performance benchmarks
|
||||
- Technical debt ratio
|
||||
- Architecture compliance
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"agents": {
|
||||
"manager": ".opencode/manager.md",
|
||||
"architekt": ".opencode/architekt.md",
|
||||
"software_engineer": ".opencode/software_engineer.md",
|
||||
"hardware_engineer": ".opencode/hardware_engineer.md",
|
||||
"tester": ".opencode/tester.md",
|
||||
"docs": ".opencode/docs.md",
|
||||
"deployment": ".opencode/deployment.md",
|
||||
"vla": ".opencode/vla.md"
|
||||
},
|
||||
"subagent_depth": 999999
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# Deployment Agent
|
||||
|
||||
## Role
|
||||
CI/CD, infrastructure, and release management.
|
||||
|
||||
## Responsibilities
|
||||
- CI/CD pipeline management
|
||||
- Infrastructure as code
|
||||
- Release orchestration
|
||||
- Environment management
|
||||
- Monitoring and alerting
|
||||
|
||||
## Key Metrics
|
||||
- Deployment frequency
|
||||
- Lead time for changes
|
||||
- Mean time to recovery
|
||||
- Change success rate
|
||||
@@ -0,0 +1,17 @@
|
||||
# Docs Agent
|
||||
|
||||
## Role
|
||||
Technical documentation, API docs, and knowledge management.
|
||||
|
||||
## Responsibilities
|
||||
- API documentation
|
||||
- User guides
|
||||
- Architecture docs
|
||||
- Change logs
|
||||
- Knowledge base maintenance
|
||||
|
||||
## Key Metrics
|
||||
- Documentation completeness
|
||||
- Update frequency
|
||||
- User satisfaction
|
||||
- Searchability
|
||||
@@ -0,0 +1,17 @@
|
||||
# Hardware Engineer Agent
|
||||
|
||||
## Role
|
||||
Hardware integration, device management, and embedded systems.
|
||||
|
||||
## Responsibilities
|
||||
- Hardware selection and integration
|
||||
- Device driver development
|
||||
- Embedded firmware
|
||||
- Prototyping and testing
|
||||
- Hardware-software interface
|
||||
|
||||
## Key Metrics
|
||||
- Hardware compatibility rate
|
||||
- Power consumption
|
||||
- Latency measurements
|
||||
- Component reliability
|
||||
@@ -0,0 +1,17 @@
|
||||
# Manager Agent
|
||||
|
||||
## Role
|
||||
Project management, task coordination, prioritization, and team oversight.
|
||||
|
||||
## Responsibilities
|
||||
- Task decomposition and assignment
|
||||
- Progress tracking and status reporting
|
||||
- Risk identification and mitigation
|
||||
- Resource allocation
|
||||
- Stakeholder communication
|
||||
|
||||
## Key Metrics
|
||||
- Sprint velocity
|
||||
- Task completion rate
|
||||
- Blockers resolved
|
||||
- Timeline adherence
|
||||
@@ -0,0 +1,98 @@
|
||||
# Carry Your Live - Project Summary
|
||||
|
||||
## Overview
|
||||
A minimalist, offline-first task management app built with Expo, React Native, and WatermelonDB. Features three primary screens: To-Do List, Add New Task, and Calendar with bottom navigation.
|
||||
|
||||
## Core Features
|
||||
- **Offline-First Architecture**: Full CRUD operations work offline with WatermelonDB + SQLite
|
||||
- **Auto-Sync**: Background synchronization with custom Postgres API when online
|
||||
- **Cross-Platform**: iOS, Android, and Web from single codebase
|
||||
|
||||
## Tech Stack
|
||||
| Layer | Technology |
|
||||
|-------|------------|
|
||||
| Framework | Expo (React Native) |
|
||||
| Language | TypeScript |
|
||||
| Navigation | Expo Router (file-based) |
|
||||
| Database | WatermelonDB + SQLite |
|
||||
| State | WatermelonDB (reactive queries, no Redux needed) |
|
||||
| Forms | React Hook Form + Zod validation |
|
||||
| UI Components | React Native Paper + Custom SVG icons |
|
||||
| Animations | React Native Reanimated |
|
||||
| Date/Time | @react-native-community/datetimepicker + date-fns |
|
||||
| Build | Gradle (local APK) |
|
||||
| CI/CD | Gitea Actions |
|
||||
|
||||
## Project Structure
|
||||
```
|
||||
carry-your-live/
|
||||
├ app/
|
||||
│ ├── _layout.tsx # Root layout + DatabaseProvider
|
||||
│ ├── (tabs)/
|
||||
│ │ ├── _layout.tsx # Bottom tab navigator
|
||||
│ │ ├── index.tsx # Tasks screen
|
||||
│ │ ├── calendar.tsx # Calendar screen
|
||||
│ │ └── settings.tsx # Settings screen
|
||||
│ └── add-task.tsx # Add Task screen
|
||||
├ src/
|
||||
│ ├── components/ # Reusable UI components
|
||||
│ ├── database/
|
||||
│ │ ├── schema.ts # WatermelonDB schema
|
||||
│ │ ├── index.ts # Database instance + collections
|
||||
│ │ └── sync.ts # Custom API sync adapter
|
||||
│ ├── models/
|
||||
│ │ ├── Task.ts
|
||||
│ │ ├── Category.ts
|
||||
│ │ └── Subtask.ts
|
||||
│ ├── hooks/
|
||||
│ │ ├── useDatabase.tsx # Database context & providers
|
||||
│ │ └── useTasks.tsx # Task queries with reactive state
|
||||
│ ├── constants/
|
||||
│ │ └── index.ts # Categories, priorities, colors
|
||||
│ └── types/
|
||||
│ └── index.ts # TypeScript interfaces
|
||||
```
|
||||
|
||||
## Data Models
|
||||
- **Task**: id, title, description, categoryId, priority, completed, dueDate, dueTime, createdAt, updatedAt, subtasks
|
||||
- **Category**: id, name, color, order
|
||||
- **Subtask**: id, taskId, title, completed, order, createdAt, updatedAt
|
||||
|
||||
## Default Categories
|
||||
| Name | Color | Order |
|
||||
|------|-------|-------|
|
||||
| Work | #E53935 (Red) | 0 |
|
||||
| Personal | #43A047 (Green) | 1 |
|
||||
| School | #1E88E5 (Blue) | 2 |
|
||||
| Shopping | #FB8C00 (Orange) | 3 |
|
||||
| Health | #8E24AA (Purple) | 4 |
|
||||
| Finance | #FDD835 (Yellow) | 5 |
|
||||
|
||||
## Priority Colors
|
||||
| Priority | Color |
|
||||
|----------|-------|
|
||||
| None | #9E9E9E |
|
||||
| Low | #43A047 |
|
||||
| Medium | #FB8C00 |
|
||||
| High | #E53935 |
|
||||
| Critical | #B71C1C |
|
||||
|
||||
## Sync API
|
||||
Custom API at `http://localhost:3000/api` (configurable in `src/database/sync.ts`):
|
||||
- **Pull**: GET /sync?since={timestamp} - returns categories, tasks, subtasks, timestamp
|
||||
- **Push**: POST /sync/push - body with changes { categories, tasks, subtasks } + lastPulledAt
|
||||
|
||||
## Getting Started
|
||||
```bash
|
||||
npm install
|
||||
npx expo start
|
||||
# Platform-specific:
|
||||
npx expo start --ios
|
||||
npx expo start --android
|
||||
npx expo start --web
|
||||
```
|
||||
|
||||
## Build Commands
|
||||
- `cd android && ./gradlew assembleDebug` - Debug APK
|
||||
- `cd android && ./gradlew assembleRelease` - Release APK
|
||||
- `npm run desktop` - Electron desktop app
|
||||
@@ -0,0 +1,17 @@
|
||||
# Software Engineer Agent
|
||||
|
||||
## Role
|
||||
Code implementation, feature development, and software quality.
|
||||
|
||||
## Responsibilities
|
||||
- Feature implementation
|
||||
- Code reviews
|
||||
- Bug fixing
|
||||
- Refactoring
|
||||
- Unit testing
|
||||
|
||||
## Key Metrics
|
||||
- Code quality score
|
||||
- Test coverage
|
||||
- Pull request throughput
|
||||
- Bug resolution time
|
||||
@@ -0,0 +1,17 @@
|
||||
# Tester Agent
|
||||
|
||||
## Role
|
||||
Quality assurance, test automation, and test strategy.
|
||||
|
||||
## Responsibilities
|
||||
- Test case design
|
||||
- Automated test execution
|
||||
- Regression testing
|
||||
- Performance testing
|
||||
- Test coverage analysis
|
||||
|
||||
## Key Metrics
|
||||
- Test pass rate
|
||||
- Defect detection rate
|
||||
- Test execution time
|
||||
- Bug leakage
|
||||
@@ -0,0 +1,17 @@
|
||||
# VLA Agent
|
||||
|
||||
## Role
|
||||
Vision-Language-Action models and multimodal understanding.
|
||||
|
||||
## Responsibilities
|
||||
- VLA model development
|
||||
- Multimodal perception
|
||||
- Action prediction
|
||||
- Policy learning
|
||||
- Sim-to-real transfer
|
||||
|
||||
## Key Metrics
|
||||
- Task success rate
|
||||
- Sample efficiency
|
||||
- Generalization ability
|
||||
- Simulation-to-real transfer rate
|
||||
@@ -57,6 +57,7 @@ GET /sync
|
||||
{
|
||||
"id": "sub_1",
|
||||
"taskId": "task_1",
|
||||
"parentSubtaskId": null,
|
||||
"title": "Setup Expo project",
|
||||
"completed": true,
|
||||
"order": 0,
|
||||
@@ -64,10 +65,21 @@ GET /sync
|
||||
"updatedAt": 1699000000000
|
||||
}
|
||||
],
|
||||
"repeatProfiles": [],
|
||||
"friendships": [],
|
||||
"deleted": [
|
||||
{
|
||||
"entity": "tasks",
|
||||
"id": "task_5",
|
||||
"updatedAt": 1699150000000
|
||||
}
|
||||
],
|
||||
"timestamp": 1699150000000
|
||||
}
|
||||
```
|
||||
|
||||
`deleted` contains every row tombstoned (deleted) since `since` — one entry per entity type/id. Clients must remove the row locally (and any related rows: a task deletion implies its subtasks, a category deletion implies its tasks and their subtasks, a parent subtask deletion implies its children).
|
||||
|
||||
**Error Responses**
|
||||
| Status | Code | Message |
|
||||
|--------|------|---------|
|
||||
@@ -114,6 +126,7 @@ POST /sync/push
|
||||
{
|
||||
"id": "sub_new",
|
||||
"taskId": "task_new",
|
||||
"parentSubtaskId": null,
|
||||
"title": "Subtask 1",
|
||||
"completed": false,
|
||||
"order": 0,
|
||||
@@ -122,10 +135,19 @@ POST /sync/push
|
||||
}
|
||||
]
|
||||
},
|
||||
"deleted": [
|
||||
{
|
||||
"entity": "categories",
|
||||
"id": "cat_gone",
|
||||
"updatedAt": 1699150000000
|
||||
}
|
||||
],
|
||||
"lastPulledAt": 1699100000000
|
||||
}
|
||||
```
|
||||
|
||||
`deleted` is a list of tombstones the client recorded locally since its last successful sync. Deletions are resolved by last-writer-wins: if the server row's `updatedAt` is newer than the tombstone's `updatedAt`, the deletion is rejected and reported as a `server_wins` conflict (with the server row as `serverVersion`) so the client re-pulls it. Accepted deletions remove the rows server-side; cascaded rows (subtasks of a deleted task, tasks/subtasks of a deleted category, children of a deleted parent subtask) are tombstoned automatically so every device removes them.
|
||||
|
||||
**Response** (200 OK)
|
||||
```json
|
||||
{
|
||||
@@ -568,6 +590,16 @@ interface Task {
|
||||
completed: boolean;
|
||||
dueDate: number; // Unix timestamp (ms), 0 if not set
|
||||
dueTime: string; // HH:MM format, empty if not set
|
||||
endTime: string; // HH:MM format, empty if not set
|
||||
allDay: boolean;
|
||||
repeat: 'none' | 'daily' | 'weekly' | 'monthly' | 'custom';
|
||||
repeatInterval: number;
|
||||
repeatDays: string; // comma separated weekday numbers, e.g. "0,2,4"
|
||||
seriesId: string; // repeating-series id, empty if not part of a series
|
||||
reminder: 'none' | 'at_time' | '15' | '30' | '60' | '120' | '1440';
|
||||
reminders: string; // comma separated reminder values, empty if none
|
||||
assigneeId: string | null;
|
||||
completedAt: number | null; // Unix timestamp (ms) when completed
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
subtasks?: Subtask[];
|
||||
@@ -579,14 +611,37 @@ interface Task {
|
||||
interface Subtask {
|
||||
id: string;
|
||||
taskId: string;
|
||||
parentSubtaskId: string | null; // id of the parent subtask, null for top-level
|
||||
title: string;
|
||||
description: string;
|
||||
priority: 'none' | 'low' | 'medium' | 'high' | 'critical';
|
||||
completed: boolean;
|
||||
dueDate: number;
|
||||
dueTime: string;
|
||||
endTime: string;
|
||||
allDay: boolean;
|
||||
repeat: 'none' | 'daily' | 'weekly' | 'monthly' | 'custom';
|
||||
repeatInterval: number;
|
||||
repeatDays: string;
|
||||
seriesId: string;
|
||||
reminder: 'none' | 'at_time' | '15' | '30' | '60' | '120' | '1440';
|
||||
reminders: string;
|
||||
assigneeId: string | null;
|
||||
order: number;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
```
|
||||
|
||||
### Tombstone
|
||||
```typescript
|
||||
interface Tombstone {
|
||||
entity: 'categories' | 'tasks' | 'subtasks' | 'repeatProfiles' | 'friendships';
|
||||
id: string; // id of the deleted row
|
||||
updatedAt: number; // Unix timestamp (ms) of the deletion
|
||||
}
|
||||
```
|
||||
|
||||
### UserSettings
|
||||
```typescript
|
||||
interface UserSettings {
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
# Carry Your Live — Roadmap & Feature List
|
||||
|
||||
Status as of Aug 2026. Based on code audit of frontend (`carry-your-live/`), backend (`backend/`), and `API_SPEC.md`.
|
||||
|
||||
## Legend
|
||||
- ✅ Implemented
|
||||
- 🟡 Partial / stubbed
|
||||
- ❌ Missing
|
||||
|
||||
---
|
||||
|
||||
## Current State (Feature List)
|
||||
|
||||
### Core Task Management
|
||||
- ✅ Tasks CRUD with title, description, category, priority, due date/time, all-day
|
||||
- ✅ Nested subtasks (sub-subtask trees), convert task↔subtask, re-parent, reorder
|
||||
- ✅ Categories/tags with colors (task color derives from category)
|
||||
- ✅ Priorities (None → Critical) with badges
|
||||
- ✅ Repeating/recurring tasks: repeat profiles, series IDs, this/future/all delete scopes, undo-complete
|
||||
- ✅ Overdue / due-today highlighting
|
||||
|
||||
### Screens & Navigation
|
||||
- ✅ Tabs: Todo, Calendar, Stats, Settings (+ Add Task, Day View, Task Detail, Subtask Detail stack screens)
|
||||
- ✅ Category filter chips, swipe actions, long-press multi-select, drag-to-reorder
|
||||
- ✅ Quick-add bar, calendar date strip, analog clock widget
|
||||
- ✅ Stats screen (client-side `useStats`)
|
||||
|
||||
### Sync & Accounts
|
||||
- ✅ Offline-first WatermelonDB (schema v21) + full sync engine (tombstones, LWW)
|
||||
- ✅ Backend: Express + Drizzle + Postgres — auth (register/login/me/forgot/reset), sync pull/push, tasks, subtasks, categories, repeat profiles, user settings, user search
|
||||
- ✅ Login/register/sign-out UI (`SyncModal`)
|
||||
|
||||
### Social
|
||||
- ✅ Friends: request/accept/decline/remove, user search, full backend routes
|
||||
- ✅ Assignee field on tasks/subtasks (schema v7)
|
||||
|
||||
### Settings (wired up)
|
||||
- ✅ Default tag, sort order, show-completed, calendar days-ahead, accent color picker, server URL override
|
||||
|
||||
---
|
||||
|
||||
## Gaps (what's broken or stubbed)
|
||||
|
||||
| Item | Status | Where |
|
||||
|---|---|---|
|
||||
| Light mode / theme toggle | ❌ Dark-only, hard-coded palette; backend has `darkMode` column unused | `src/theme.tsx:92` |
|
||||
| Notifications toggle | 🟡 Stored but never read; toggling off doesn't disable reminders | `settings.tsx:148`, `src/services/notifications.ts` |
|
||||
| Global reminder preference | 🟡 Stored but never applied when scheduling | `src/theme.tsx:180` |
|
||||
| Forgot/reset password UI | 🟡 Backend done, no client UI | `backend/src/routes/auth.ts:149` |
|
||||
| Pull-to-refresh | 🟡 Reloads local queries only, doesn't trigger server sync | `TaskList.tsx:129` |
|
||||
| Client settings sync | 🟡 All settings AsyncStorage-only; `PATCH /users/me/settings` never called | — |
|
||||
| Reminder enum mismatch | 🟡 Spec uses minutes (`'15'`,`'30'`…), client uses `'5m'`,`'1h'` style | `API_SPEC.md` vs client |
|
||||
| Webhooks / rate-limit headers | 🟡 In spec, not built | `API_SPEC.md` |
|
||||
|
||||
---
|
||||
|
||||
## Roadmap (by release)
|
||||
|
||||
> Last released: **v1.0.0** (2026-08-05). The `Unreleased` changelog section already contains the friends feature, assignee field, and various fixes.
|
||||
|
||||
---
|
||||
|
||||
### v1.1.0 — Friends & Assignees (staged; ship current `Unreleased` + stub fixes)
|
||||
|
||||
Ships what's already merged plus fixes to half-wired features so nothing in the release is a dead toggle.
|
||||
|
||||
- 📦 Release existing `Unreleased` changes: friends feature, assignee field (schema v7), QuickAddBar on overview, sync fixes for repeat/reminders
|
||||
- Fix notifications toggle so it's actually respected (`src/services/notifications.ts`); wire up `rescheduleAllReminders()`
|
||||
- Apply global reminder preference when scheduling task reminders
|
||||
- Make pull-to-refresh run a real server sync cycle (`TaskList.tsx:129`)
|
||||
- Add forgot/reset password UI (backend routes already exist: `backend/src/routes/auth.ts:149`)
|
||||
- Unify reminder offset enums between client, `API_SPEC.md`, and backend
|
||||
|
||||
**Theme:** "Everything you can toggle does something."
|
||||
|
||||
---
|
||||
|
||||
### v1.2.0 — Theming & Findability
|
||||
|
||||
- Light mode + follow-system-theme option; theme-mode setting synced via `/users/me/settings` (backend `darkMode` column already exists)
|
||||
- Sync all client settings to `PATCH /users/me/settings` instead of AsyncStorage-only
|
||||
- Task search: filter-by-text in list + global search screen
|
||||
- Friend-request badges/indicators in main UI (polling-based)
|
||||
- Resolve API error-format drift between spec and auth routes
|
||||
|
||||
**Theme:** "Look good, find anything."
|
||||
|
||||
---
|
||||
|
||||
### v1.3.0 — Stats & Insights
|
||||
|
||||
- Persisted stats snapshots for history/trends over time (current stats are per-local-data only)
|
||||
- Productivity trends: completion rates by category/priority, streaks
|
||||
- Export stats (CSV/image share)
|
||||
|
||||
**Theme:** "Show me how I'm doing."
|
||||
|
||||
---
|
||||
|
||||
### v1.4.0 — Quick Capture & Calendar UX
|
||||
|
||||
Client-side only; no backend changes required.
|
||||
|
||||
- Natural language quick-add ("pay rent tomorrow 5pm !finance" → parsed fields) in QuickAddBar
|
||||
- Snooze/postpone gesture on overdue tasks
|
||||
- Drag-and-drop rescheduling between days in calendar
|
||||
- Month grid view alongside the date strip
|
||||
- "Today at a glance" dashboard combining overdue, today, and stats
|
||||
- Time estimates + daily planned-vs-free-time indicator
|
||||
|
||||
**Theme:** "Capture in seconds."
|
||||
|
||||
---
|
||||
|
||||
### v1.5.0 — Power Tools & Personalization
|
||||
|
||||
- Task templates / saved routines (builds on repeat profiles)
|
||||
- Soft-archive with search (archive instead of delete)
|
||||
- Full local JSON export/import backup
|
||||
- Attachments/links/photos on tasks
|
||||
- Location-based reminders (expo-location)
|
||||
- Markdown support in descriptions
|
||||
- Deep links (`carryyourlive://task/<id>`) for shortcuts and notifications
|
||||
- Undo toast for all destructive actions (extend existing complete-undo)
|
||||
- Biometric lock / privacy blur on app switch
|
||||
- Haptics settings + reduced-motion mode
|
||||
- Custom app icon / theme presets beyond accent color
|
||||
- Onboarding flow + sample data seeding
|
||||
- Android home-screen widget / week agenda widget
|
||||
- import calendar from other apps
|
||||
|
||||
**Theme:** "Make it yours, make it fast."
|
||||
|
||||
---
|
||||
|
||||
### v2.0.0 — Collaboration
|
||||
|
||||
Breaking-ish social features; requires backend schema changes.
|
||||
|
||||
- Real task assignment delivery: shared-list or assignment mechanism (rows are currently strictly user-scoped — assigneeId goes nowhere)
|
||||
- Shared lists / collaborative categories with permissions
|
||||
- Server push notifications (FCM/APNs): cross-device reminders, friend requests, assignments
|
||||
- Comments/activity feed on assigned tasks
|
||||
- Shared category templates ("family shopping list")
|
||||
- Reactions/acknowledgments on assignments
|
||||
|
||||
**Theme:** "Carry it together."
|
||||
|
||||
---
|
||||
|
||||
### v2.1.0 — Platform & Ops
|
||||
|
||||
- Webhooks (`POST /webhooks/sync`) + documented rate limiting per `API_SPEC.md`
|
||||
- Account management: email field, account deletion
|
||||
- Release hardening: OTA update flow validation, EAS builds for iOS/Android
|
||||
|
||||
**Theme:** "Production-grade."
|
||||
@@ -1,3 +1,5 @@
|
||||
export declare const ENTITIES: readonly ["categories", "tasks", "subtasks", "repeatProfiles", "friendships"];
|
||||
export type EntityName = (typeof ENTITIES)[number];
|
||||
export declare const users: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
||||
name: "users";
|
||||
schema: undefined;
|
||||
@@ -50,6 +52,38 @@ export declare const users: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
resetToken: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "reset_token";
|
||||
tableName: "users";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: false;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
resetTokenExpiry: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "reset_token_expiry";
|
||||
tableName: "users";
|
||||
dataType: "number";
|
||||
columnType: "PgBigInt53";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: false;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "created_at";
|
||||
tableName: "users";
|
||||
@@ -334,7 +368,7 @@ export declare const tasks: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
notNull: false;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
@@ -455,6 +489,22 @@ export declare const tasks: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
allDay: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "all_day";
|
||||
tableName: "tasks";
|
||||
dataType: "boolean";
|
||||
columnType: "PgBoolean";
|
||||
data: boolean;
|
||||
driverParam: boolean;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
repeat: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "repeat";
|
||||
tableName: "tasks";
|
||||
@@ -551,6 +601,38 @@ export declare const tasks: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
reminders: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "reminders";
|
||||
tableName: "tasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
completedAt: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "completed_at";
|
||||
tableName: "tasks";
|
||||
dataType: "number";
|
||||
columnType: "PgBigInt53";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: false;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "created_at";
|
||||
tableName: "tasks";
|
||||
@@ -586,6 +668,436 @@ export declare const tasks: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
||||
};
|
||||
dialect: "pg";
|
||||
}>;
|
||||
export declare const tombstones: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
||||
name: "tombstones";
|
||||
schema: undefined;
|
||||
columns: {
|
||||
entity: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "entity";
|
||||
tableName: "tombstones";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: "categories" | "tasks" | "subtasks" | "repeatProfiles" | "friendships";
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: ["categories", "tasks", "subtasks", "repeatProfiles", "friendships"];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
entityId: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "entity_id";
|
||||
tableName: "tombstones";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
userId: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "user_id";
|
||||
tableName: "tombstones";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
updatedAt: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "updated_at";
|
||||
tableName: "tombstones";
|
||||
dataType: "number";
|
||||
columnType: "PgBigInt53";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
};
|
||||
dialect: "pg";
|
||||
}>;
|
||||
export declare const subtasks: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
||||
name: "subtasks";
|
||||
schema: undefined;
|
||||
columns: {
|
||||
id: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "id";
|
||||
tableName: "subtasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: true;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
userId: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "user_id";
|
||||
tableName: "subtasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
taskId: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "task_id";
|
||||
tableName: "subtasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
parentSubtaskId: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "parent_subtask_id";
|
||||
tableName: "subtasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: false;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
title: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "title";
|
||||
tableName: "subtasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
description: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "description";
|
||||
tableName: "subtasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
priority: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "priority";
|
||||
tableName: "subtasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: "none" | "low" | "medium" | "high" | "critical";
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: ["none", "low", "medium", "high", "critical"];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
completed: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "completed";
|
||||
tableName: "subtasks";
|
||||
dataType: "boolean";
|
||||
columnType: "PgBoolean";
|
||||
data: boolean;
|
||||
driverParam: boolean;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
dueDate: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "due_date";
|
||||
tableName: "subtasks";
|
||||
dataType: "number";
|
||||
columnType: "PgBigInt53";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
dueTime: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "due_time";
|
||||
tableName: "subtasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
endTime: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "end_time";
|
||||
tableName: "subtasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
allDay: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "all_day";
|
||||
tableName: "subtasks";
|
||||
dataType: "boolean";
|
||||
columnType: "PgBoolean";
|
||||
data: boolean;
|
||||
driverParam: boolean;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
repeat: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "repeat";
|
||||
tableName: "subtasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: "custom" | "none" | "daily" | "weekly" | "monthly";
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: ["none", "daily", "weekly", "monthly", "custom"];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
repeatInterval: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "repeat_interval";
|
||||
tableName: "subtasks";
|
||||
dataType: "number";
|
||||
columnType: "PgInteger";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
repeatDays: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "repeat_days";
|
||||
tableName: "subtasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
seriesId: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "series_id";
|
||||
tableName: "subtasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
reminder: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "reminder";
|
||||
tableName: "subtasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440";
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: ["none", "at_time", "15", "30", "60", "120", "1440"];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
reminders: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "reminders";
|
||||
tableName: "subtasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
assigneeId: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "assignee_id";
|
||||
tableName: "subtasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: false;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
order: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "order";
|
||||
tableName: "subtasks";
|
||||
dataType: "number";
|
||||
columnType: "PgInteger";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "created_at";
|
||||
tableName: "subtasks";
|
||||
dataType: "number";
|
||||
columnType: "PgBigInt53";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
updatedAt: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "updated_at";
|
||||
tableName: "subtasks";
|
||||
dataType: "number";
|
||||
columnType: "PgBigInt53";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
};
|
||||
dialect: "pg";
|
||||
}>;
|
||||
export declare const repeatProfiles: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
||||
name: "repeat_profiles";
|
||||
schema: undefined;
|
||||
@@ -721,141 +1233,6 @@ export declare const repeatProfiles: import("drizzle-orm/pg-core").PgTableWithCo
|
||||
};
|
||||
dialect: "pg";
|
||||
}>;
|
||||
export declare const subtasks: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
||||
name: "subtasks";
|
||||
schema: undefined;
|
||||
columns: {
|
||||
id: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "id";
|
||||
tableName: "subtasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: true;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
userId: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "user_id";
|
||||
tableName: "subtasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
taskId: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "task_id";
|
||||
tableName: "subtasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
title: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "title";
|
||||
tableName: "subtasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
completed: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "completed";
|
||||
tableName: "subtasks";
|
||||
dataType: "boolean";
|
||||
columnType: "PgBoolean";
|
||||
data: boolean;
|
||||
driverParam: boolean;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
order: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "order";
|
||||
tableName: "subtasks";
|
||||
dataType: "number";
|
||||
columnType: "PgInteger";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "created_at";
|
||||
tableName: "subtasks";
|
||||
dataType: "number";
|
||||
columnType: "PgBigInt53";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
updatedAt: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "updated_at";
|
||||
tableName: "subtasks";
|
||||
dataType: "number";
|
||||
columnType: "PgBigInt53";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
};
|
||||
dialect: "pg";
|
||||
}>;
|
||||
export declare const userSettings: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
||||
name: "user_settings";
|
||||
schema: undefined;
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/db/schema.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAKhB,CAAC;AAEH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAevB,CAAC;AAEF,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAQrB,CAAC;AAEH,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmBhB,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EASzB,CAAC;AAEH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EASnB,CAAC;AAEH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EASvB,CAAC"}
|
||||
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/db/schema.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,QAAQ,+EAAgF,CAAC;AACtG,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnD,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAOhB,CAAC;AAEH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAevB,CAAC;AAEF,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAQrB,CAAC;AAEH,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsBhB,CAAC;AAEH,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAYtB,CAAC;AAGF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2BlB,CAAC;AAEJ,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EASzB,CAAC;AAEH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EASvB,CAAC"}
|
||||
@@ -1,12 +1,15 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.userSettings = exports.subtasks = exports.repeatProfiles = exports.tasks = exports.categories = exports.friendships = exports.users = void 0;
|
||||
exports.userSettings = exports.repeatProfiles = exports.subtasks = exports.tombstones = exports.tasks = exports.categories = exports.friendships = exports.users = exports.ENTITIES = void 0;
|
||||
const pg_core_1 = require("drizzle-orm/pg-core");
|
||||
const drizzle_orm_1 = require("drizzle-orm");
|
||||
exports.ENTITIES = ['categories', 'tasks', 'subtasks', 'repeatProfiles', 'friendships'];
|
||||
exports.users = (0, pg_core_1.pgTable)('users', {
|
||||
id: (0, pg_core_1.text)('id').primaryKey(),
|
||||
username: (0, pg_core_1.text)('username').notNull().unique(),
|
||||
passwordHash: (0, pg_core_1.text)('password_hash').notNull(),
|
||||
resetToken: (0, pg_core_1.text)('reset_token'),
|
||||
resetTokenExpiry: (0, pg_core_1.bigint)('reset_token_expiry', { mode: 'number' }),
|
||||
createdAt: (0, pg_core_1.bigint)('created_at', { mode: 'number' }).notNull().default((0, drizzle_orm_1.sql) `EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
});
|
||||
exports.friendships = (0, pg_core_1.pgTable)('friendships', {
|
||||
@@ -33,7 +36,7 @@ exports.categories = (0, pg_core_1.pgTable)('categories', {
|
||||
exports.tasks = (0, pg_core_1.pgTable)('tasks', {
|
||||
id: (0, pg_core_1.text)('id').primaryKey(),
|
||||
userId: (0, pg_core_1.text)('user_id').notNull().references(() => exports.users.id, { onDelete: 'cascade' }),
|
||||
categoryId: (0, pg_core_1.text)('category_id').notNull().references(() => exports.categories.id, { onDelete: 'cascade' }),
|
||||
categoryId: (0, pg_core_1.text)('category_id').references(() => exports.categories.id, { onDelete: 'cascade' }),
|
||||
title: (0, pg_core_1.text)('title').notNull(),
|
||||
description: (0, pg_core_1.text)('description').notNull().default(''),
|
||||
priority: (0, pg_core_1.text)('priority', { enum: ['none', 'low', 'medium', 'high', 'critical'] }).notNull().default('none'),
|
||||
@@ -41,15 +44,56 @@ exports.tasks = (0, pg_core_1.pgTable)('tasks', {
|
||||
dueDate: (0, pg_core_1.bigint)('due_date', { mode: 'number' }).notNull().default(0),
|
||||
dueTime: (0, pg_core_1.text)('due_time').notNull().default(''),
|
||||
endTime: (0, pg_core_1.text)('end_time').notNull().default(''),
|
||||
allDay: (0, pg_core_1.boolean)('all_day').notNull().default(false),
|
||||
repeat: (0, pg_core_1.text)('repeat', { enum: ['none', 'daily', 'weekly', 'monthly', 'custom'] }).notNull().default('none'),
|
||||
repeatInterval: (0, pg_core_1.integer)('repeat_interval').notNull().default(1),
|
||||
repeatDays: (0, pg_core_1.text)('repeat_days').notNull().default(''),
|
||||
seriesId: (0, pg_core_1.text)('series_id').notNull().default(''),
|
||||
assigneeId: (0, pg_core_1.text)('assignee_id').references(() => exports.users.id, { onDelete: 'set null' }),
|
||||
reminder: (0, pg_core_1.text)('reminder', { enum: ['none', 'at_time', '15', '30', '60', '120', '1440'] }).notNull().default('none'),
|
||||
reminders: (0, pg_core_1.text)('reminders').notNull().default(''),
|
||||
completedAt: (0, pg_core_1.bigint)('completed_at', { mode: 'number' }),
|
||||
createdAt: (0, pg_core_1.bigint)('created_at', { mode: 'number' }).notNull().default((0, drizzle_orm_1.sql) `EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
updatedAt: (0, pg_core_1.bigint)('updated_at', { mode: 'number' }).notNull().default((0, drizzle_orm_1.sql) `EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
});
|
||||
exports.tombstones = (0, pg_core_1.pgTable)('tombstones', {
|
||||
entity: (0, pg_core_1.text)('entity', { enum: exports.ENTITIES }).notNull(),
|
||||
entityId: (0, pg_core_1.text)('entity_id').notNull(),
|
||||
userId: (0, pg_core_1.text)('user_id').notNull().references(() => exports.users.id, { onDelete: 'cascade' }),
|
||||
updatedAt: (0, pg_core_1.bigint)('updated_at', { mode: 'number' }).notNull(),
|
||||
}, (t) => ({
|
||||
pk: (0, pg_core_1.primaryKey)({ columns: [t.entity, t.entityId] }),
|
||||
userIdx: (0, pg_core_1.index)('tombstones_user_updated_idx').on(t.userId, t.updatedAt),
|
||||
}));
|
||||
// Define subtasks with explicit type to avoid circular reference
|
||||
exports.subtasks = (0, pg_core_1.pgTable)('subtasks', {
|
||||
id: (0, pg_core_1.text)('id').primaryKey(),
|
||||
userId: (0, pg_core_1.text)('user_id').notNull().references(() => exports.users.id, { onDelete: 'cascade' }),
|
||||
taskId: (0, pg_core_1.text)('task_id').notNull().references(() => exports.tasks.id, { onDelete: 'cascade' }),
|
||||
parentSubtaskId: (0, pg_core_1.text)('parent_subtask_id').references(() => exports.subtasks.id, { onDelete: 'cascade' }),
|
||||
title: (0, pg_core_1.text)('title').notNull(),
|
||||
description: (0, pg_core_1.text)('description').notNull().default(''),
|
||||
priority: (0, pg_core_1.text)('priority', { enum: ['none', 'low', 'medium', 'high', 'critical'] }).notNull().default('none'),
|
||||
completed: (0, pg_core_1.boolean)('completed').notNull().default(false),
|
||||
dueDate: (0, pg_core_1.bigint)('due_date', { mode: 'number' }).notNull().default(0),
|
||||
dueTime: (0, pg_core_1.text)('due_time').notNull().default(''),
|
||||
endTime: (0, pg_core_1.text)('end_time').notNull().default(''),
|
||||
allDay: (0, pg_core_1.boolean)('all_day').notNull().default(false),
|
||||
repeat: (0, pg_core_1.text)('repeat', { enum: ['none', 'daily', 'weekly', 'monthly', 'custom'] }).notNull().default('none'),
|
||||
repeatInterval: (0, pg_core_1.integer)('repeat_interval').notNull().default(1),
|
||||
repeatDays: (0, pg_core_1.text)('repeat_days').notNull().default(''),
|
||||
seriesId: (0, pg_core_1.text)('series_id').notNull().default(''),
|
||||
reminder: (0, pg_core_1.text)('reminder', { enum: ['none', 'at_time', '15', '30', '60', '120', '1440'] }).notNull().default('none'),
|
||||
reminders: (0, pg_core_1.text)('reminders').notNull().default(''),
|
||||
assigneeId: (0, pg_core_1.text)('assignee_id').references(() => exports.users.id, { onDelete: 'set null' }),
|
||||
order: (0, pg_core_1.integer)('order').notNull().default(0),
|
||||
createdAt: (0, pg_core_1.bigint)('created_at', { mode: 'number' }).notNull().default((0, drizzle_orm_1.sql) `EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
updatedAt: (0, pg_core_1.bigint)('updated_at', { mode: 'number' }).notNull().default((0, drizzle_orm_1.sql) `EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
}, (t) => ({
|
||||
parentIdx: (0, pg_core_1.index)('subtasks_parent_idx').on(t.parentSubtaskId),
|
||||
taskIdx: (0, pg_core_1.index)('subtasks_task_idx').on(t.taskId),
|
||||
userIdx: (0, pg_core_1.index)('subtasks_user_idx').on(t.userId),
|
||||
}));
|
||||
exports.repeatProfiles = (0, pg_core_1.pgTable)('repeat_profiles', {
|
||||
id: (0, pg_core_1.text)('id').primaryKey(),
|
||||
userId: (0, pg_core_1.text)('user_id').notNull().references(() => exports.users.id, { onDelete: 'cascade' }),
|
||||
@@ -60,16 +104,6 @@ exports.repeatProfiles = (0, pg_core_1.pgTable)('repeat_profiles', {
|
||||
createdAt: (0, pg_core_1.bigint)('created_at', { mode: 'number' }).notNull().default((0, drizzle_orm_1.sql) `EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
updatedAt: (0, pg_core_1.bigint)('updated_at', { mode: 'number' }).notNull().default((0, drizzle_orm_1.sql) `EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
});
|
||||
exports.subtasks = (0, pg_core_1.pgTable)('subtasks', {
|
||||
id: (0, pg_core_1.text)('id').primaryKey(),
|
||||
userId: (0, pg_core_1.text)('user_id').notNull().references(() => exports.users.id, { onDelete: 'cascade' }),
|
||||
taskId: (0, pg_core_1.text)('task_id').notNull().references(() => exports.tasks.id, { onDelete: 'cascade' }),
|
||||
title: (0, pg_core_1.text)('title').notNull(),
|
||||
completed: (0, pg_core_1.boolean)('completed').notNull().default(false),
|
||||
order: (0, pg_core_1.integer)('order').notNull().default(0),
|
||||
createdAt: (0, pg_core_1.bigint)('created_at', { mode: 'number' }).notNull().default((0, drizzle_orm_1.sql) `EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
updatedAt: (0, pg_core_1.bigint)('updated_at', { mode: 'number' }).notNull().default((0, drizzle_orm_1.sql) `EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
});
|
||||
exports.userSettings = (0, pg_core_1.pgTable)('user_settings', {
|
||||
userId: (0, pg_core_1.text)('user_id').primaryKey().references(() => exports.users.id, { onDelete: 'cascade' }),
|
||||
darkMode: (0, pg_core_1.boolean)('dark_mode').notNull().default(false),
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,CAAC;AAYvB,QAAA,MAAM,GAAG,6CAAY,CAAC;AAkCtB,eAAe,GAAG,CAAC"}
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,CAAC;AAavB,QAAA,MAAM,GAAG,6CAAY,CAAC;AAmCtB,eAAe,GAAG,CAAC"}
|
||||
@@ -12,6 +12,7 @@ const tasks_1 = __importDefault(require("./routes/tasks"));
|
||||
const subtasks_1 = __importDefault(require("./routes/subtasks"));
|
||||
const users_1 = __importDefault(require("./routes/users"));
|
||||
const friends_1 = __importDefault(require("./routes/friends"));
|
||||
const repeatProfiles_1 = __importDefault(require("./routes/repeatProfiles"));
|
||||
const sync_1 = __importDefault(require("./routes/sync"));
|
||||
const errorHandler_1 = require("./middleware/errorHandler");
|
||||
const app = (0, express_1.default)();
|
||||
@@ -31,6 +32,7 @@ app.use('/api/categories', categories_1.default);
|
||||
app.use('/api/tasks', tasks_1.default);
|
||||
app.use('/api/subtasks', subtasks_1.default);
|
||||
app.use('/api/users', users_1.default);
|
||||
app.use('/api/repeat-profiles', repeatProfiles_1.default);
|
||||
app.use('/api/sync', sync_1.default);
|
||||
app.use('/api/friends', friends_1.default);
|
||||
// 404 handler
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;AAAA,yBAAuB;AACvB,sDAA8B;AAC9B,gDAAwB;AACxB,yDAAuC;AACvC,qEAAiD;AACjD,2DAAwC;AACxC,iEAA8C;AAC9C,2DAAwC;AACxC,+DAA4C;AAC5C,yDAAuC;AACvC,4DAA0E;AAE1E,MAAM,GAAG,GAAG,IAAA,iBAAO,GAAE,CAAC;AACtB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC;AAEtC,GAAG,CAAC,GAAG,CAAC,IAAA,cAAI,EAAC;IACX,MAAM,EAAE,IAAI;IACZ,WAAW,EAAE,IAAI;CAClB,CAAC,CAAC,CAAC;AACJ,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,IAAI,EAAE,CAAC,CAAC;AAExB,eAAe;AACf,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IAC9B,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;AACpD,CAAC,CAAC,CAAC;AAEH,aAAa;AACb,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,cAAU,CAAC,CAAC;AACjC,GAAG,CAAC,GAAG,CAAC,iBAAiB,EAAE,oBAAc,CAAC,CAAC;AAC3C,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,eAAU,CAAC,CAAC;AAClC,GAAG,CAAC,GAAG,CAAC,eAAe,EAAE,kBAAa,CAAC,CAAC;AACxC,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,eAAU,CAAC,CAAC;AAClC,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,cAAU,CAAC,CAAC;AACjC,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,iBAAY,CAAC,CAAC;AAEtC,cAAc;AACd,GAAG,CAAC,GAAG,CAAC,8BAAe,CAAC,CAAC;AAEzB,gBAAgB;AAChB,GAAG,CAAC,GAAG,CAAC,2BAAY,CAAC,CAAC;AAEtB,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;IACpB,OAAO,CAAC,GAAG,CAAC,yCAAyC,IAAI,EAAE,CAAC,CAAC;IAC7D,OAAO,CAAC,GAAG,CAAC,wCAAwC,IAAI,MAAM,CAAC,CAAC;AAClE,CAAC,CAAC,CAAC;AAEH,kBAAe,GAAG,CAAC"}
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;AAAA,yBAAuB;AACvB,sDAA8B;AAC9B,gDAAwB;AACxB,yDAAuC;AACvC,qEAAiD;AACjD,2DAAwC;AACxC,iEAA8C;AAC9C,2DAAwC;AACxC,+DAA4C;AAC5C,6EAA0D;AAC1D,yDAAuC;AACvC,4DAA0E;AAE1E,MAAM,GAAG,GAAG,IAAA,iBAAO,GAAE,CAAC;AACtB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC;AAEtC,GAAG,CAAC,GAAG,CAAC,IAAA,cAAI,EAAC;IACX,MAAM,EAAE,IAAI;IACZ,WAAW,EAAE,IAAI;CAClB,CAAC,CAAC,CAAC;AACJ,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,IAAI,EAAE,CAAC,CAAC;AAExB,eAAe;AACf,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IAC9B,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;AACpD,CAAC,CAAC,CAAC;AAEH,aAAa;AACb,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,cAAU,CAAC,CAAC;AACjC,GAAG,CAAC,GAAG,CAAC,iBAAiB,EAAE,oBAAc,CAAC,CAAC;AAC3C,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,eAAU,CAAC,CAAC;AAClC,GAAG,CAAC,GAAG,CAAC,eAAe,EAAE,kBAAa,CAAC,CAAC;AACxC,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,eAAU,CAAC,CAAC;AAClC,GAAG,CAAC,GAAG,CAAC,sBAAsB,EAAE,wBAAmB,CAAC,CAAC;AACrD,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,cAAU,CAAC,CAAC;AACjC,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,iBAAY,CAAC,CAAC;AAEtC,cAAc;AACd,GAAG,CAAC,GAAG,CAAC,8BAAe,CAAC,CAAC;AAEzB,gBAAgB;AAChB,GAAG,CAAC,GAAG,CAAC,2BAAY,CAAC,CAAC;AAEtB,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;IACpB,OAAO,CAAC,GAAG,CAAC,yCAAyC,IAAI,EAAE,CAAC,CAAC;IAC7D,OAAO,CAAC,GAAG,CAAC,wCAAwC,IAAI,MAAM,CAAC,CAAC;AAClE,CAAC,CAAC,CAAC;AAEH,kBAAe,GAAG,CAAC"}
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/routes/auth.ts"],"names":[],"mappings":"AASA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAmFxB,eAAe,MAAM,CAAC"}
|
||||
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/routes/auth.ts"],"names":[],"mappings":"AAYA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA0LxB,eAAe,MAAM,CAAC"}
|
||||
@@ -1,4 +1,7 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const express_1 = require("express");
|
||||
const asyncHandler_1 = require("../utils/asyncHandler");
|
||||
@@ -8,7 +11,47 @@ const drizzle_orm_1 = require("drizzle-orm");
|
||||
const auth_1 = require("../utils/auth");
|
||||
const errorHandler_1 = require("../middleware/errorHandler");
|
||||
const zod_1 = require("zod");
|
||||
const bcryptjs_1 = __importDefault(require("bcryptjs"));
|
||||
const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
|
||||
const crypto_1 = __importDefault(require("crypto"));
|
||||
const router = (0, express_1.Router)();
|
||||
// Rate limiting for auth endpoints
|
||||
const authLimiter = (0, express_rate_limit_1.default)({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 10, // limit each IP to 10 requests per windowMs
|
||||
message: {
|
||||
error: {
|
||||
code: 'RATE_LIMITED',
|
||||
message: 'Too many attempts. Please try again later.',
|
||||
},
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
const loginLimiter = (0, express_rate_limit_1.default)({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 5, // stricter limit for login
|
||||
message: {
|
||||
error: {
|
||||
code: 'RATE_LIMITED',
|
||||
message: 'Too many login attempts. Please try again later.',
|
||||
},
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
const passwordResetLimiter = (0, express_rate_limit_1.default)({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
max: 3, // very strict for password reset
|
||||
message: {
|
||||
error: {
|
||||
code: 'RATE_LIMITED',
|
||||
message: 'Too many password reset requests. Please try again later.',
|
||||
},
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
const usernameSchema = zod_1.z
|
||||
.string()
|
||||
.min(3)
|
||||
@@ -22,15 +65,24 @@ const loginSchema = zod_1.z.object({
|
||||
username: zod_1.z.string(),
|
||||
password: zod_1.z.string(),
|
||||
});
|
||||
// In production, use bcrypt or argon2 for password hashing
|
||||
function hashPassword(password) {
|
||||
// Simple hash for demo - replace with bcrypt in production
|
||||
return Buffer.from(password).toString('base64');
|
||||
const forgotPasswordSchema = zod_1.z.object({
|
||||
username: zod_1.z.string().min(1),
|
||||
});
|
||||
const resetPasswordSchema = zod_1.z.object({
|
||||
token: zod_1.z.string().min(1),
|
||||
password: zod_1.z.string().min(8),
|
||||
});
|
||||
const BCRYPT_ROUNDS = 12;
|
||||
async function hashPassword(password) {
|
||||
return bcryptjs_1.default.hash(password, BCRYPT_ROUNDS);
|
||||
}
|
||||
function verifyPassword(password, hash) {
|
||||
return hashPassword(password) === hash;
|
||||
async function verifyPassword(password, hash) {
|
||||
return bcryptjs_1.default.compare(password, hash);
|
||||
}
|
||||
router.post('/register', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
function generateResetToken() {
|
||||
return crypto_1.default.randomBytes(32).toString('hex');
|
||||
}
|
||||
router.post('/register', authLimiter, (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const data = registerSchema.parse(req.body);
|
||||
const existing = await db_1.db.select().from(schema_1.users).where((0, drizzle_orm_1.eq)(schema_1.users.username, data.username)).limit(1);
|
||||
if (existing.length > 0) {
|
||||
@@ -41,7 +93,7 @@ router.post('/register', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
await db_1.db.insert(schema_1.users).values({
|
||||
id: userId,
|
||||
username: data.username,
|
||||
passwordHash: hashPassword(data.password),
|
||||
passwordHash: await hashPassword(data.password),
|
||||
createdAt: now,
|
||||
});
|
||||
const token = (0, auth_1.generateToken)({ userId, username: data.username });
|
||||
@@ -50,7 +102,7 @@ router.post('/register', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
token,
|
||||
});
|
||||
}));
|
||||
router.post('/login', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
router.post('/login', loginLimiter, (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const data = loginSchema.parse(req.body);
|
||||
const user = await db_1.db.select().from(schema_1.users).where((0, drizzle_orm_1.eq)(schema_1.users.username, data.username)).limit(1);
|
||||
if (user.length === 0 || !verifyPassword(data.password, user[0].passwordHash)) {
|
||||
@@ -72,5 +124,45 @@ router.get('/me', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
}
|
||||
res.json({ id: user[0].id, username: user[0].username });
|
||||
}));
|
||||
router.post('/forgot-password', passwordResetLimiter, (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const data = forgotPasswordSchema.parse(req.body);
|
||||
const user = await db_1.db.select().from(schema_1.users).where((0, drizzle_orm_1.eq)(schema_1.users.username, data.username)).limit(1);
|
||||
// Always return success to prevent username enumeration
|
||||
if (user.length === 0) {
|
||||
res.json({ success: true });
|
||||
return;
|
||||
}
|
||||
const resetToken = generateResetToken();
|
||||
const resetTokenExpiry = Date.now() + 3600000; // 1 hour
|
||||
await db_1.db
|
||||
.update(schema_1.users)
|
||||
.set({ resetToken, resetTokenExpiry })
|
||||
.where((0, drizzle_orm_1.eq)(schema_1.users.id, user[0].id));
|
||||
// In production, send email with reset link
|
||||
// For now, return token in response (dev only)
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
res.json({ success: true, resetToken });
|
||||
}
|
||||
else {
|
||||
res.json({ success: true });
|
||||
}
|
||||
}));
|
||||
router.post('/reset-password', passwordResetLimiter, (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const data = resetPasswordSchema.parse(req.body);
|
||||
const user = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.users)
|
||||
.where((0, drizzle_orm_1.eq)(schema_1.users.resetToken, data.token))
|
||||
.limit(1);
|
||||
if (user.length === 0 || !user[0].resetTokenExpiry || user[0].resetTokenExpiry < Date.now()) {
|
||||
throw new errorHandler_1.AppError('INVALID_TOKEN', 'Invalid or expired reset token', 400);
|
||||
}
|
||||
const newPasswordHash = await hashPassword(data.password);
|
||||
await db_1.db
|
||||
.update(schema_1.users)
|
||||
.set({ passwordHash: newPasswordHash, resetToken: null, resetTokenExpiry: null })
|
||||
.where((0, drizzle_orm_1.eq)(schema_1.users.id, user[0].id));
|
||||
res.json({ success: true });
|
||||
}));
|
||||
exports.default = router;
|
||||
//# sourceMappingURL=auth.js.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"friends.d.ts","sourceRoot":"","sources":["../../src/routes/friends.ts"],"names":[],"mappings":"AAUA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAsKxB,eAAe,MAAM,CAAC"}
|
||||
{"version":3,"file":"friends.d.ts","sourceRoot":"","sources":["../../src/routes/friends.ts"],"names":[],"mappings":"AAUA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAuLxB,eAAe,MAAM,CAAC"}
|
||||
@@ -53,6 +53,17 @@ router.get('/', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
}));
|
||||
res.json({ friends, incoming, outgoing });
|
||||
}));
|
||||
router.get('/search', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const { q } = validation_1.searchQuerySchema.parse(req.query);
|
||||
const userId = req.user.userId;
|
||||
const results = await db_1.db
|
||||
.select({ id: schema_1.users.id, username: schema_1.users.username })
|
||||
.from(schema_1.users)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.ne)(schema_1.users.id, userId), (0, drizzle_orm_1.ilike)(schema_1.users.username, `%${q}%`)))
|
||||
.orderBy((0, drizzle_orm_1.asc)(schema_1.users.username))
|
||||
.limit(20);
|
||||
res.json(results);
|
||||
}));
|
||||
router.post('/requests', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const { username } = validation_1.friendRequestSchema.parse(req.body);
|
||||
const userId = req.user.userId;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
declare const router: import("express-serve-static-core").Router;
|
||||
export default router;
|
||||
//# sourceMappingURL=repeatProfiles.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"repeatProfiles.d.ts","sourceRoot":"","sources":["../../src/routes/repeatProfiles.ts"],"names":[],"mappings":"AAUA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA+ExB,eAAe,MAAM,CAAC"}
|
||||
@@ -0,0 +1,74 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const express_1 = require("express");
|
||||
const asyncHandler_1 = require("../utils/asyncHandler");
|
||||
const db_1 = require("../db");
|
||||
const schema_1 = require("../db/schema");
|
||||
const drizzle_orm_1 = require("drizzle-orm");
|
||||
const auth_1 = require("../utils/auth");
|
||||
const auth_2 = require("../utils/auth");
|
||||
const errorHandler_1 = require("../middleware/errorHandler");
|
||||
const validation_1 = require("../utils/validation");
|
||||
const router = (0, express_1.Router)();
|
||||
router.use(auth_1.authMiddleware);
|
||||
router.get('/', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const profiles = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.repeatProfiles)
|
||||
.where((0, drizzle_orm_1.eq)(schema_1.repeatProfiles.userId, req.user.userId))
|
||||
.orderBy((0, drizzle_orm_1.asc)(schema_1.repeatProfiles.createdAt));
|
||||
res.json({ profiles });
|
||||
}));
|
||||
router.post('/', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const data = validation_1.repeatProfileSchema.parse(req.body);
|
||||
const userId = req.user.userId;
|
||||
const now = Date.now();
|
||||
const newProfile = {
|
||||
id: (0, auth_2.generateId)('rp'),
|
||||
userId,
|
||||
name: data.name,
|
||||
repeat: data.repeat,
|
||||
repeatInterval: data.repeatInterval,
|
||||
repeatDays: data.repeatDays,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
await db_1.db.insert(schema_1.repeatProfiles).values(newProfile);
|
||||
res.status(201).json(newProfile);
|
||||
}));
|
||||
router.patch('/:id', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const data = validation_1.repeatProfileUpdateSchema.parse(req.body);
|
||||
const userId = req.user.userId;
|
||||
const existing = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.repeatProfiles)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.repeatProfiles.id, req.params.id), (0, drizzle_orm_1.eq)(schema_1.repeatProfiles.userId, userId)))
|
||||
.limit(1);
|
||||
if (existing.length === 0) {
|
||||
throw new errorHandler_1.AppError('NOT_FOUND', 'Repeat profile not found', 404);
|
||||
}
|
||||
const now = Date.now();
|
||||
const updated = await db_1.db
|
||||
.update(schema_1.repeatProfiles)
|
||||
.set({ ...data, updatedAt: now })
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.repeatProfiles.id, req.params.id), (0, drizzle_orm_1.eq)(schema_1.repeatProfiles.userId, userId)))
|
||||
.returning();
|
||||
res.json(updated[0]);
|
||||
}));
|
||||
router.delete('/:id', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const userId = req.user.userId;
|
||||
const existing = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.repeatProfiles)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.repeatProfiles.id, req.params.id), (0, drizzle_orm_1.eq)(schema_1.repeatProfiles.userId, userId)))
|
||||
.limit(1);
|
||||
if (existing.length === 0) {
|
||||
throw new errorHandler_1.AppError('NOT_FOUND', 'Repeat profile not found', 404);
|
||||
}
|
||||
await db_1.db
|
||||
.delete(schema_1.repeatProfiles)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.repeatProfiles.id, req.params.id), (0, drizzle_orm_1.eq)(schema_1.repeatProfiles.userId, userId)));
|
||||
res.status(204).send();
|
||||
}));
|
||||
exports.default = router;
|
||||
//# sourceMappingURL=repeatProfiles.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"repeatProfiles.js","sourceRoot":"","sources":["../../src/routes/repeatProfiles.ts"],"names":[],"mappings":";;AAAA,qCAAoD;AACpD,wDAAqD;AACrD,8BAA2B;AAC3B,yCAA8C;AAC9C,6CAA2C;AAC3C,wCAA+C;AAC/C,wCAA2C;AAC3C,6DAAsD;AACtD,oDAAqF;AAErF,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAExB,MAAM,CAAC,GAAG,CAAC,qBAAc,CAAC,CAAC;AAE3B,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,IAAA,2BAAY,EAAC,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,EAAE;IACjE,MAAM,QAAQ,GAAG,MAAM,OAAE;SACtB,MAAM,EAAE;SACR,IAAI,CAAC,uBAAc,CAAC;SACpB,KAAK,CAAC,IAAA,gBAAE,EAAC,uBAAc,CAAC,MAAM,EAAE,GAAG,CAAC,IAAK,CAAC,MAAM,CAAC,CAAC;SAClD,OAAO,CAAC,IAAA,iBAAG,EAAC,uBAAc,CAAC,SAAS,CAAC,CAAC,CAAC;IAE1C,GAAG,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;AACzB,CAAC,CAAC,CAAC,CAAC;AAEJ,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,IAAA,2BAAY,EAAC,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,EAAE;IAClE,MAAM,IAAI,GAAG,gCAAmB,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACjD,MAAM,MAAM,GAAG,GAAG,CAAC,IAAK,CAAC,MAAM,CAAC;IAChC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAEvB,MAAM,UAAU,GAAG;QACjB,EAAE,EAAE,IAAA,iBAAU,EAAC,IAAI,CAAC;QACpB,MAAM;QACN,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,cAAc,EAAE,IAAI,CAAC,cAAc;QACnC,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,SAAS,EAAE,GAAG;QACd,SAAS,EAAE,GAAG;KACf,CAAC;IAEF,MAAM,OAAE,CAAC,MAAM,CAAC,uBAAc,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAEnD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;AACnC,CAAC,CAAC,CAAC,CAAC;AAEJ,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,IAAA,2BAAY,EAAC,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,EAAE;IACtE,MAAM,IAAI,GAAG,sCAAyB,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACvD,MAAM,MAAM,GAAG,GAAG,CAAC,IAAK,CAAC,MAAM,CAAC;IAEhC,MAAM,QAAQ,GAAG,MAAM,OAAE;SACtB,MAAM,EAAE;SACR,IAAI,CAAC,uBAAc,CAAC;SACpB,KAAK,CAAC,IAAA,iBAAG,EAAC,IAAA,gBAAE,EAAC,uBAAc,CAAC,EAAE,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,IAAA,gBAAE,EAAC,uBAAc,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;SACnF,KAAK,CAAC,CAAC,CAAC,CAAC;IAEZ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,uBAAQ,CAAC,WAAW,EAAE,0BAA0B,EAAE,GAAG,CAAC,CAAC;IACnE,CAAC;IAED,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,MAAM,OAAO,GAAG,MAAM,OAAE;SACrB,MAAM,CAAC,uBAAc,CAAC;SACtB,GAAG,CAAC,EAAE,GAAG,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC;SAChC,KAAK,CAAC,IAAA,iBAAG,EAAC,IAAA,gBAAE,EAAC,uBAAc,CAAC,EAAE,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,IAAA,gBAAE,EAAC,uBAAc,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;SACnF,SAAS,EAAE,CAAC;IAEf,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC,CAAC;AAEJ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,IAAA,2BAAY,EAAC,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,EAAE;IACvE,MAAM,MAAM,GAAG,GAAG,CAAC,IAAK,CAAC,MAAM,CAAC;IAEhC,MAAM,QAAQ,GAAG,MAAM,OAAE;SACtB,MAAM,EAAE;SACR,IAAI,CAAC,uBAAc,CAAC;SACpB,KAAK,CAAC,IAAA,iBAAG,EAAC,IAAA,gBAAE,EAAC,uBAAc,CAAC,EAAE,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,IAAA,gBAAE,EAAC,uBAAc,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;SACnF,KAAK,CAAC,CAAC,CAAC,CAAC;IAEZ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,uBAAQ,CAAC,WAAW,EAAE,0BAA0B,EAAE,GAAG,CAAC,CAAC;IACnE,CAAC;IAED,MAAM,OAAE;SACL,MAAM,CAAC,uBAAc,CAAC;SACtB,KAAK,CAAC,IAAA,iBAAG,EAAC,IAAA,gBAAE,EAAC,uBAAc,CAAC,EAAE,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,IAAA,gBAAE,EAAC,uBAAc,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;IAEvF,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AACzB,CAAC,CAAC,CAAC,CAAC;AAEJ,kBAAe,MAAM,CAAC"}
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"subtasks.d.ts","sourceRoot":"","sources":["../../src/routes/subtasks.ts"],"names":[],"mappings":"AASA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAmIxB,eAAe,MAAM,CAAC"}
|
||||
{"version":3,"file":"subtasks.d.ts","sourceRoot":"","sources":["../../src/routes/subtasks.ts"],"names":[],"mappings":"AASA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAoKxB,eAAe,MAAM,CAAC"}
|
||||
@@ -10,6 +10,16 @@ const errorHandler_1 = require("../middleware/errorHandler");
|
||||
const validation_1 = require("../utils/validation");
|
||||
const router = (0, express_1.Router)();
|
||||
router.use(auth_1.authMiddleware);
|
||||
// Helper to build nested subtask tree
|
||||
const buildSubtaskTree = (allSubtasks, parentId = null) => {
|
||||
return allSubtasks
|
||||
.filter((s) => s.parentSubtaskId === parentId)
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.map((s) => ({
|
||||
...s,
|
||||
subtasks: buildSubtaskTree(allSubtasks, s.id),
|
||||
}));
|
||||
};
|
||||
router.get('/task/:taskId', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const userId = req.user.userId;
|
||||
// Verify task exists and belongs to user
|
||||
@@ -26,7 +36,8 @@ router.get('/task/:taskId', (0, asyncHandler_1.asyncHandler)(async (req, res) =>
|
||||
.from(schema_1.subtasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.subtasks.taskId, req.params.taskId), (0, drizzle_orm_1.eq)(schema_1.subtasks.userId, userId)))
|
||||
.orderBy((0, drizzle_orm_1.asc)(schema_1.subtasks.order));
|
||||
res.json({ subtasks: taskSubtasks });
|
||||
const nestedSubtasks = buildSubtaskTree(taskSubtasks);
|
||||
res.json({ subtasks: nestedSubtasks });
|
||||
}));
|
||||
router.post('/task/:taskId', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const data = validation_1.subtaskCreateSchema.parse(req.body);
|
||||
@@ -40,10 +51,11 @@ router.post('/task/:taskId', (0, asyncHandler_1.asyncHandler)(async (req, res) =
|
||||
throw new errorHandler_1.AppError('NOT_FOUND', 'Task not found', 404);
|
||||
}
|
||||
const now = Date.now();
|
||||
const parentSubtaskId = data.parentSubtaskId || null;
|
||||
const maxOrder = await db_1.db
|
||||
.select({ order: schema_1.subtasks.order })
|
||||
.from(schema_1.subtasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.subtasks.taskId, req.params.taskId), (0, drizzle_orm_1.eq)(schema_1.subtasks.userId, userId)))
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.subtasks.taskId, req.params.taskId), (0, drizzle_orm_1.eq)(schema_1.subtasks.userId, userId), parentSubtaskId ? (0, drizzle_orm_1.eq)(schema_1.subtasks.parentSubtaskId, parentSubtaskId) : (0, drizzle_orm_1.isNull)(schema_1.subtasks.parentSubtaskId)))
|
||||
.orderBy((0, drizzle_orm_1.desc)(schema_1.subtasks.order))
|
||||
.limit(1);
|
||||
const subtaskId = `sub_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
||||
@@ -51,8 +63,22 @@ router.post('/task/:taskId', (0, asyncHandler_1.asyncHandler)(async (req, res) =
|
||||
id: subtaskId,
|
||||
userId,
|
||||
taskId: req.params.taskId,
|
||||
parentSubtaskId,
|
||||
title: data.title,
|
||||
description: data.description ?? '',
|
||||
priority: data.priority ?? 'none',
|
||||
completed: false,
|
||||
dueDate: data.dueDate ?? 0,
|
||||
dueTime: data.dueTime ?? '',
|
||||
endTime: data.endTime ?? '',
|
||||
allDay: data.allDay ?? false,
|
||||
repeat: data.repeat ?? 'none',
|
||||
repeatInterval: data.repeatInterval ?? 1,
|
||||
repeatDays: data.repeatDays ?? '',
|
||||
seriesId: data.seriesId ?? '',
|
||||
reminder: data.reminder ?? 'none',
|
||||
reminders: data.reminders ?? '',
|
||||
assigneeId: data.assigneeId ?? null,
|
||||
order: data.order ?? (maxOrder[0]?.order ?? -1) + 1,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/routes/sync.ts"],"names":[],"mappings":"AASA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA8SxB,eAAe,MAAM,CAAC"}
|
||||
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/routes/sync.ts"],"names":[],"mappings":"AASA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA2lBxB,eAAe,MAAM,CAAC"}
|
||||
@@ -27,9 +27,8 @@ router.get('/', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId), (0, drizzle_orm_1.gte)(schema_1.tasks.updatedAt, sinceDate)))
|
||||
.orderBy((0, drizzle_orm_1.asc)(schema_1.tasks.updatedAt));
|
||||
// Fetch subtasks changed since timestamp
|
||||
const taskIds = changedTasks.map(t => t.id);
|
||||
let changedSubtasks = [];
|
||||
if (taskIds.length > 0) {
|
||||
if (changedTasks.length > 0) {
|
||||
changedSubtasks = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.subtasks)
|
||||
@@ -37,7 +36,7 @@ router.get('/', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
.orderBy((0, drizzle_orm_1.asc)(schema_1.subtasks.updatedAt));
|
||||
}
|
||||
else {
|
||||
// Also fetch subtasks for tasks that might have been deleted (we track by updatedAt)
|
||||
// Also fetch subtasks for tasks that might have been deleted
|
||||
changedSubtasks = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.subtasks)
|
||||
@@ -56,6 +55,12 @@ router.get('/', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
.from(schema_1.friendships)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.or)((0, drizzle_orm_1.eq)(schema_1.friendships.userId, userId), (0, drizzle_orm_1.eq)(schema_1.friendships.friendId, userId)), (0, drizzle_orm_1.gte)(schema_1.friendships.updatedAt, sinceDate)))
|
||||
.orderBy((0, drizzle_orm_1.asc)(schema_1.friendships.updatedAt));
|
||||
// Fetch tombstones (deletions) changed since timestamp
|
||||
const changedTombstones = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.tombstones)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tombstones.userId, userId), (0, drizzle_orm_1.gte)(schema_1.tombstones.updatedAt, sinceDate)))
|
||||
.orderBy((0, drizzle_orm_1.asc)(schema_1.tombstones.updatedAt));
|
||||
const timestamp = Date.now();
|
||||
res.json({
|
||||
categories: changedCategories,
|
||||
@@ -63,6 +68,7 @@ router.get('/', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
subtasks: changedSubtasks,
|
||||
repeatProfiles: changedRepeatProfiles,
|
||||
friendships: changedFriendships,
|
||||
deleted: changedTombstones.map((t) => ({ entity: t.entity, id: t.entityId, updatedAt: t.updatedAt })),
|
||||
timestamp,
|
||||
});
|
||||
}));
|
||||
@@ -73,6 +79,65 @@ router.post('/push', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const timestamp = Date.now();
|
||||
try {
|
||||
await db_1.db.transaction(async (tx) => {
|
||||
// Ensure every referenced category exists (FK integrity) so a stale or
|
||||
// never-synced category reference cannot fail the entire push. Missing
|
||||
// categories are recreated as a fallback and the client heals on pull.
|
||||
const referencedCategoryIds = new Set();
|
||||
for (const task of data.changes.tasks ?? []) {
|
||||
if (task.categoryId)
|
||||
referencedCategoryIds.add(task.categoryId);
|
||||
}
|
||||
for (const sub of data.changes.subtasks ?? []) {
|
||||
const task = (data.changes.tasks ?? []).find((t) => t.id === sub.taskId);
|
||||
if (task?.categoryId)
|
||||
referencedCategoryIds.add(task.categoryId);
|
||||
}
|
||||
if (referencedCategoryIds.size > 0) {
|
||||
const existing = await tx
|
||||
.select({ id: schema_1.categories.id })
|
||||
.from(schema_1.categories)
|
||||
.where((0, drizzle_orm_1.inArray)(schema_1.categories.id, [...referencedCategoryIds]));
|
||||
const existingIds = new Set(existing.map((c) => c.id));
|
||||
const missing = [...referencedCategoryIds].filter((id) => !existingIds.has(id));
|
||||
if (missing.length > 0) {
|
||||
const rows = await tx
|
||||
.select({ max: (0, drizzle_orm_1.sql) `max(${schema_1.categories.order})` })
|
||||
.from(schema_1.categories)
|
||||
.where((0, drizzle_orm_1.eq)(schema_1.categories.userId, userId));
|
||||
const startOrder = (rows[0]?.max ?? -1) + 1;
|
||||
await tx.insert(schema_1.categories).values(missing.map((id, i) => ({
|
||||
id,
|
||||
userId,
|
||||
name: 'Default',
|
||||
color: '#9E9E9E',
|
||||
order: startOrder + i,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
})));
|
||||
}
|
||||
}
|
||||
// Sanitize assignee references: only real user ids may be stored (FK).
|
||||
// Stale/unknown assignee ids are silently dropped to null instead of
|
||||
// failing the whole push transaction.
|
||||
const assigneeIds = new Set();
|
||||
for (const task of data.changes.tasks ?? []) {
|
||||
if (task.assigneeId)
|
||||
assigneeIds.add(task.assigneeId);
|
||||
}
|
||||
for (const sub of data.changes.subtasks ?? []) {
|
||||
if (sub.assigneeId)
|
||||
assigneeIds.add(sub.assigneeId);
|
||||
}
|
||||
const validAssignees = new Set();
|
||||
if (assigneeIds.size > 0) {
|
||||
const rows = await tx
|
||||
.select({ id: schema_1.users.id })
|
||||
.from(schema_1.users)
|
||||
.where((0, drizzle_orm_1.inArray)(schema_1.users.id, [...assigneeIds]));
|
||||
for (const r of rows)
|
||||
validAssignees.add(r.id);
|
||||
}
|
||||
const sanitizeAssignee = (a) => a && validAssignees.has(a) ? a : null;
|
||||
// Process categories
|
||||
if (data.changes.categories && data.changes.categories.length > 0) {
|
||||
for (const cat of data.changes.categories) {
|
||||
@@ -131,6 +196,20 @@ router.post('/push', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
});
|
||||
continue; // Server wins
|
||||
}
|
||||
// Prevent completing tasks with future due dates
|
||||
if (task.completed === true) {
|
||||
const effectiveDueDate = task.dueDate ?? existing[0].dueDate;
|
||||
if (!(0, validation_1.canCompleteTask)(effectiveDueDate)) {
|
||||
conflicts.push({
|
||||
entity: 'tasks',
|
||||
id: task.id,
|
||||
serverVersion: existing[0],
|
||||
clientVersion: task,
|
||||
resolution: 'server_wins',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
await tx
|
||||
.update(schema_1.tasks)
|
||||
.set({
|
||||
@@ -140,14 +219,17 @@ router.post('/push', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
priority: task.priority,
|
||||
completed: task.completed,
|
||||
dueDate: task.dueDate,
|
||||
dueTime: task.dueTime,
|
||||
dueTime: task.dueTime ?? '',
|
||||
endTime: task.endTime ?? '',
|
||||
allDay: task.allDay ?? false,
|
||||
repeat: task.repeat ?? 'none',
|
||||
repeatInterval: task.repeatInterval ?? 1,
|
||||
repeatDays: task.repeatDays ?? '',
|
||||
seriesId: task.seriesId ?? '',
|
||||
reminder: task.reminder ?? 'none',
|
||||
assigneeId: task.assigneeId ?? null,
|
||||
reminders: task.reminders ?? '',
|
||||
assigneeId: sanitizeAssignee(task.assigneeId),
|
||||
completedAt: task.completedAt ?? null,
|
||||
updatedAt: task.updatedAt,
|
||||
})
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.id, task.id), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)));
|
||||
@@ -155,6 +237,8 @@ router.post('/push', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
else {
|
||||
await tx.insert(schema_1.tasks).values({
|
||||
...task,
|
||||
assigneeId: sanitizeAssignee(task.assigneeId),
|
||||
allDay: task.allDay ?? false,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
@@ -184,8 +268,22 @@ router.post('/push', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
.update(schema_1.subtasks)
|
||||
.set({
|
||||
taskId: sub.taskId,
|
||||
parentSubtaskId: sub.parentSubtaskId ?? null,
|
||||
title: sub.title,
|
||||
description: sub.description ?? '',
|
||||
priority: sub.priority ?? 'none',
|
||||
completed: sub.completed,
|
||||
dueDate: sub.dueDate ?? 0,
|
||||
dueTime: sub.dueTime ?? '',
|
||||
endTime: sub.endTime ?? '',
|
||||
allDay: sub.allDay ?? false,
|
||||
repeat: sub.repeat ?? 'none',
|
||||
repeatInterval: sub.repeatInterval ?? 1,
|
||||
repeatDays: sub.repeatDays ?? '',
|
||||
seriesId: sub.seriesId ?? '',
|
||||
reminder: sub.reminder ?? 'none',
|
||||
reminders: sub.reminders ?? '',
|
||||
assigneeId: sanitizeAssignee(sub.assigneeId),
|
||||
order: sub.order,
|
||||
updatedAt: sub.updatedAt,
|
||||
})
|
||||
@@ -194,6 +292,7 @@ router.post('/push', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
else {
|
||||
await tx.insert(schema_1.subtasks).values({
|
||||
...sub,
|
||||
assigneeId: sanitizeAssignee(sub.assigneeId),
|
||||
userId,
|
||||
});
|
||||
}
|
||||
@@ -273,6 +372,13 @@ router.post('/push', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Process deletions (tombstones) - last so they see the state produced
|
||||
// by the upserts above and resolve by last-writer-wins.
|
||||
if (data.deleted && data.deleted.length > 0) {
|
||||
for (const deleted of data.deleted) {
|
||||
await applyTombstone(tx, deleted.entity, deleted.id, deleted.updatedAt, userId, conflicts);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
@@ -285,5 +391,175 @@ router.post('/push', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
conflicts,
|
||||
});
|
||||
}));
|
||||
// Upsert a tombstone row, keeping the latest updatedAt.
|
||||
async function upsertTombstone(tx, entity, entityId, updatedAt, userId) {
|
||||
const existing = await tx
|
||||
.select({ updatedAt: schema_1.tombstones.updatedAt })
|
||||
.from(schema_1.tombstones)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tombstones.entity, entity), (0, drizzle_orm_1.eq)(schema_1.tombstones.entityId, entityId)))
|
||||
.limit(1);
|
||||
const merged = Math.max(existing[0]?.updatedAt ?? 0, updatedAt);
|
||||
if (existing.length > 0) {
|
||||
await tx
|
||||
.update(schema_1.tombstones)
|
||||
.set({ updatedAt: merged })
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tombstones.entity, entity), (0, drizzle_orm_1.eq)(schema_1.tombstones.entityId, entityId)));
|
||||
}
|
||||
else {
|
||||
await tx.insert(schema_1.tombstones).values({ entity, entityId, userId, updatedAt: merged });
|
||||
}
|
||||
}
|
||||
// Apply a client deletion. LWW: if the server row is newer than the deletion
|
||||
// timestamp, the deletion is rejected (server_wins conflict) so the client
|
||||
// re-pulls the row. Accepted deletions cascade tombstones to every FK-cascaded
|
||||
// child so all devices remove them too.
|
||||
async function applyTombstone(tx, entity, id, deletedAt, userId, conflicts) {
|
||||
const tombstoneOf = (e, ids) => ids.forEach((i) => upsertTombstone(tx, e, i, deletedAt, userId));
|
||||
if (entity === 'tasks') {
|
||||
const row = await tx
|
||||
.select()
|
||||
.from(schema_1.tasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.id, id), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)))
|
||||
.limit(1);
|
||||
if (row.length === 0) {
|
||||
await upsertTombstone(tx, entity, id, deletedAt, userId);
|
||||
return;
|
||||
}
|
||||
if (row[0].updatedAt > deletedAt) {
|
||||
conflicts.push({
|
||||
entity: 'tasks',
|
||||
id,
|
||||
serverVersion: row[0],
|
||||
clientVersion: { id, updatedAt: deletedAt },
|
||||
resolution: 'server_wins',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const children = await tx
|
||||
.select({ id: schema_1.subtasks.id })
|
||||
.from(schema_1.subtasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.subtasks.taskId, id), (0, drizzle_orm_1.eq)(schema_1.subtasks.userId, userId)));
|
||||
tombstoneOf('subtasks', children.map((c) => c.id));
|
||||
await tx.delete(schema_1.tasks).where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.id, id), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)));
|
||||
await upsertTombstone(tx, entity, id, deletedAt, userId);
|
||||
return;
|
||||
}
|
||||
if (entity === 'categories') {
|
||||
const row = await tx
|
||||
.select()
|
||||
.from(schema_1.categories)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.categories.id, id), (0, drizzle_orm_1.eq)(schema_1.categories.userId, userId)))
|
||||
.limit(1);
|
||||
if (row.length === 0) {
|
||||
await upsertTombstone(tx, entity, id, deletedAt, userId);
|
||||
return;
|
||||
}
|
||||
if (row[0].updatedAt > deletedAt) {
|
||||
conflicts.push({
|
||||
entity: 'categories',
|
||||
id,
|
||||
serverVersion: row[0],
|
||||
clientVersion: { id, updatedAt: deletedAt },
|
||||
resolution: 'server_wins',
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Deleting the category cascades its tasks (and their subtasks) -
|
||||
// tombstone all of them so every client removes them.
|
||||
const catTasks = await tx
|
||||
.select()
|
||||
.from(schema_1.tasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.categoryId, id), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)));
|
||||
for (const taskRow of catTasks) {
|
||||
const subIds = await tx
|
||||
.select({ id: schema_1.subtasks.id })
|
||||
.from(schema_1.subtasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.subtasks.taskId, taskRow.id), (0, drizzle_orm_1.eq)(schema_1.subtasks.userId, userId)));
|
||||
tombstoneOf('subtasks', subIds.map((s) => s.id));
|
||||
tombstoneOf('tasks', [taskRow.id]);
|
||||
}
|
||||
await tx.delete(schema_1.categories).where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.categories.id, id), (0, drizzle_orm_1.eq)(schema_1.categories.userId, userId)));
|
||||
await upsertTombstone(tx, entity, id, deletedAt, userId);
|
||||
return;
|
||||
}
|
||||
if (entity === 'subtasks') {
|
||||
const row = await tx
|
||||
.select()
|
||||
.from(schema_1.subtasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.subtasks.id, id), (0, drizzle_orm_1.eq)(schema_1.subtasks.userId, userId)))
|
||||
.limit(1);
|
||||
if (row.length === 0) {
|
||||
await upsertTombstone(tx, entity, id, deletedAt, userId);
|
||||
return;
|
||||
}
|
||||
if (row[0].updatedAt > deletedAt) {
|
||||
conflicts.push({
|
||||
entity: 'subtasks',
|
||||
id,
|
||||
serverVersion: row[0],
|
||||
clientVersion: { id, updatedAt: deletedAt },
|
||||
resolution: 'server_wins',
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Deleting a parent subtask cascades its children in PG - tombstone them.
|
||||
const children = await tx
|
||||
.select({ id: schema_1.subtasks.id })
|
||||
.from(schema_1.subtasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.subtasks.parentSubtaskId, id), (0, drizzle_orm_1.eq)(schema_1.subtasks.userId, userId)));
|
||||
tombstoneOf('subtasks', children.map((c) => c.id));
|
||||
await tx.delete(schema_1.subtasks).where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.subtasks.id, id), (0, drizzle_orm_1.eq)(schema_1.subtasks.userId, userId)));
|
||||
await upsertTombstone(tx, entity, id, deletedAt, userId);
|
||||
return;
|
||||
}
|
||||
if (entity === 'repeatProfiles') {
|
||||
const row = await tx
|
||||
.select()
|
||||
.from(schema_1.repeatProfiles)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.repeatProfiles.id, id), (0, drizzle_orm_1.eq)(schema_1.repeatProfiles.userId, userId)))
|
||||
.limit(1);
|
||||
if (row.length === 0) {
|
||||
await upsertTombstone(tx, entity, id, deletedAt, userId);
|
||||
return;
|
||||
}
|
||||
if (row[0].updatedAt > deletedAt) {
|
||||
conflicts.push({
|
||||
entity: 'repeatProfiles',
|
||||
id,
|
||||
serverVersion: row[0],
|
||||
clientVersion: { id, updatedAt: deletedAt },
|
||||
resolution: 'server_wins',
|
||||
});
|
||||
return;
|
||||
}
|
||||
await tx.delete(schema_1.repeatProfiles).where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.repeatProfiles.id, id), (0, drizzle_orm_1.eq)(schema_1.repeatProfiles.userId, userId)));
|
||||
await upsertTombstone(tx, entity, id, deletedAt, userId);
|
||||
return;
|
||||
}
|
||||
if (entity === 'friendships') {
|
||||
const row = await tx
|
||||
.select()
|
||||
.from(schema_1.friendships)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.friendships.id, id), (0, drizzle_orm_1.or)((0, drizzle_orm_1.eq)(schema_1.friendships.userId, userId), (0, drizzle_orm_1.eq)(schema_1.friendships.friendId, userId))))
|
||||
.limit(1);
|
||||
if (row.length === 0) {
|
||||
await upsertTombstone(tx, entity, id, deletedAt, userId);
|
||||
return;
|
||||
}
|
||||
if (row[0].updatedAt > deletedAt) {
|
||||
conflicts.push({
|
||||
entity: 'friendships',
|
||||
id,
|
||||
serverVersion: row[0],
|
||||
clientVersion: { id, updatedAt: deletedAt },
|
||||
resolution: 'server_wins',
|
||||
});
|
||||
return;
|
||||
}
|
||||
await tx.delete(schema_1.friendships).where((0, drizzle_orm_1.eq)(schema_1.friendships.id, id));
|
||||
await upsertTombstone(tx, entity, id, deletedAt, userId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
exports.default = router;
|
||||
//# sourceMappingURL=sync.js.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../src/routes/tasks.ts"],"names":[],"mappings":"AAUA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA8RxB,eAAe,MAAM,CAAC"}
|
||||
{"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../src/routes/tasks.ts"],"names":[],"mappings":"AAUA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA+TxB,eAAe,MAAM,CAAC"}
|
||||
@@ -97,14 +97,16 @@ router.post('/', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const data = validation_1.taskCreateSchema.parse(req.body);
|
||||
const userId = req.user.userId;
|
||||
const now = Date.now();
|
||||
// Verify category exists and belongs to user
|
||||
const cat = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.categories)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.categories.id, data.categoryId), (0, drizzle_orm_1.eq)(schema_1.categories.userId, userId)))
|
||||
.limit(1);
|
||||
if (cat.length === 0) {
|
||||
throw new errorHandler_1.AppError('NOT_FOUND', 'Category not found', 404);
|
||||
// Verify category exists and belongs to user (optional - tasks may be uncategorized)
|
||||
if (data.categoryId) {
|
||||
const cat = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.categories)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.categories.id, data.categoryId), (0, drizzle_orm_1.eq)(schema_1.categories.userId, userId)))
|
||||
.limit(1);
|
||||
if (cat.length === 0) {
|
||||
throw new errorHandler_1.AppError('NOT_FOUND', 'Category not found', 404);
|
||||
}
|
||||
}
|
||||
const taskId = `task_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
||||
const newTask = {
|
||||
@@ -118,7 +120,10 @@ router.post('/', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
dueDate: data.dueDate ?? 0,
|
||||
dueTime: data.dueTime ?? '',
|
||||
endTime: data.endTime ?? '',
|
||||
allDay: data.allDay ?? false,
|
||||
assigneeId: data.assigneeId ?? null,
|
||||
reminder: data.reminder ?? 'none',
|
||||
reminders: data.reminders ?? '',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
@@ -166,6 +171,13 @@ router.patch('/:id', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
throw new errorHandler_1.AppError('NOT_FOUND', 'Category not found', 404);
|
||||
}
|
||||
}
|
||||
// Prevent completing tasks with future due dates
|
||||
if (data.completed === true) {
|
||||
const effectiveDueDate = data.dueDate ?? existing[0].dueDate;
|
||||
if (!(0, validation_1.canCompleteTask)(effectiveDueDate)) {
|
||||
throw new errorHandler_1.AppError('VALIDATION_ERROR', 'Cannot mark a task as completed if its due date is in the future', 400);
|
||||
}
|
||||
}
|
||||
const now = Date.now();
|
||||
const updated = await db_1.db
|
||||
.update(schema_1.tasks)
|
||||
@@ -207,13 +219,15 @@ router.post('/batch', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
for (const op of operations) {
|
||||
try {
|
||||
if (op.type === 'create') {
|
||||
const cat = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.categories)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.categories.id, op.data.categoryId), (0, drizzle_orm_1.eq)(schema_1.categories.userId, userId)))
|
||||
.limit(1);
|
||||
if (cat.length === 0)
|
||||
throw new errorHandler_1.AppError('NOT_FOUND', 'Category not found', 404);
|
||||
if (op.data.categoryId) {
|
||||
const cat = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.categories)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.categories.id, op.data.categoryId), (0, drizzle_orm_1.eq)(schema_1.categories.userId, userId)))
|
||||
.limit(1);
|
||||
if (cat.length === 0)
|
||||
throw new errorHandler_1.AppError('NOT_FOUND', 'Category not found', 404);
|
||||
}
|
||||
const taskId = `task_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
||||
const now = Date.now();
|
||||
await db_1.db.insert(schema_1.tasks).values({
|
||||
@@ -227,6 +241,25 @@ router.post('/batch', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
results.push({ id: taskId, success: true });
|
||||
}
|
||||
else if (op.type === 'update') {
|
||||
if (op.data.completed === true) {
|
||||
let effectiveDueDate;
|
||||
if (op.data.dueDate !== undefined) {
|
||||
effectiveDueDate = op.data.dueDate;
|
||||
}
|
||||
else {
|
||||
const existingTask = await db_1.db
|
||||
.select({ dueDate: schema_1.tasks.dueDate })
|
||||
.from(schema_1.tasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.id, op.id), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)))
|
||||
.limit(1);
|
||||
if (existingTask.length === 0)
|
||||
throw new errorHandler_1.AppError('NOT_FOUND', 'Task not found', 404);
|
||||
effectiveDueDate = existingTask[0].dueDate;
|
||||
}
|
||||
if (!(0, validation_1.canCompleteTask)(effectiveDueDate)) {
|
||||
throw new errorHandler_1.AppError('VALIDATION_ERROR', 'Cannot mark a task as completed if its due date is in the future', 400);
|
||||
}
|
||||
}
|
||||
await db_1.db
|
||||
.update(schema_1.tasks)
|
||||
.set({ ...op.data, updatedAt: Date.now() })
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Request, Response, NextFunction } from 'express';
|
||||
import { AuthPayload } from '../types';
|
||||
export declare function generateToken(payload: AuthPayload): string;
|
||||
export declare function verifyToken(token: string): AuthPayload | null;
|
||||
export declare function authMiddleware(req: Request, res: Response, next: NextFunction): void;
|
||||
export declare function authMiddleware(req: Request, res: Response, next: NextFunction): Promise<void>;
|
||||
export declare function optionalAuthMiddleware(req: Request, res: Response, next: NextFunction): void;
|
||||
export declare function generateId(prefix?: string): string;
|
||||
export declare function getCurrentTimestamp(): number;
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/utils/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE1D,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAKvC,wBAAgB,aAAa,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAE1D;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAM7D;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CA4BpF;AAED,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CAW5F;AAED,wBAAgB,UAAU,CAAC,MAAM,GAAE,MAAW,GAAG,MAAM,CAItD;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C"}
|
||||
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/utils/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAK1D,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAKvC,wBAAgB,aAAa,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAE1D;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAM7D;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAuCnG;AAED,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CAW5F;AAED,wBAAgB,UAAU,CAAC,MAAM,GAAE,MAAW,GAAG,MAAM,CAItD;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C"}
|
||||
@@ -10,6 +10,9 @@ exports.optionalAuthMiddleware = optionalAuthMiddleware;
|
||||
exports.generateId = generateId;
|
||||
exports.getCurrentTimestamp = getCurrentTimestamp;
|
||||
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
||||
const db_1 = require("../db");
|
||||
const schema_1 = require("../db/schema");
|
||||
const drizzle_orm_1 = require("drizzle-orm");
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-super-secret-jwt-key-change-in-production';
|
||||
const JWT_EXPIRES_IN = '7d';
|
||||
function generateToken(payload) {
|
||||
@@ -23,7 +26,7 @@ function verifyToken(token) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function authMiddleware(req, res, next) {
|
||||
async function authMiddleware(req, res, next) {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
res.status(401).json({
|
||||
@@ -45,6 +48,16 @@ function authMiddleware(req, res, next) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
const user = await db_1.db.select({ id: schema_1.users.id }).from(schema_1.users).where((0, drizzle_orm_1.eq)(schema_1.users.id, payload.userId)).limit(1);
|
||||
if (user.length === 0) {
|
||||
res.status(401).json({
|
||||
error: {
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'User no longer exists, please log in again',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
req.user = payload;
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/utils/auth.ts"],"names":[],"mappings":";;;;;AAOA,sCAEC;AAED,kCAMC;AAED,wCA4BC;AAED,wDAWC;AAED,gCAIC;AAED,kDAEC;AArED,gEAA+B;AAG/B,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,gDAAgD,CAAC;AAC9F,MAAM,cAAc,GAAG,IAAI,CAAC;AAE5B,SAAgB,aAAa,CAAC,OAAoB;IAChD,OAAO,sBAAG,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,SAAS,EAAE,cAAc,EAAE,CAAC,CAAC;AACtE,CAAC;AAED,SAAgB,WAAW,CAAC,KAAa;IACvC,IAAI,CAAC;QACH,OAAO,sBAAG,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU,CAAgB,CAAC;IACtD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAgB,cAAc,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IAC5E,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;IAE7C,IAAI,CAAC,UAAU,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACrD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YACnB,KAAK,EAAE;gBACL,IAAI,EAAE,cAAc;gBACpB,OAAO,EAAE,yCAAyC;aACnD;SACF,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAClC,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAEnC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YACnB,KAAK,EAAE;gBACL,IAAI,EAAE,cAAc;gBACpB,OAAO,EAAE,0BAA0B;aACpC;SACF,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,GAAG,CAAC,IAAI,GAAG,OAAO,CAAC;IACnB,IAAI,EAAE,CAAC;AACT,CAAC;AAED,SAAgB,sBAAsB,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IACpF,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;IAE7C,IAAI,UAAU,IAAI,UAAU,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACnD,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAClC,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;QACnC,IAAI,OAAO,EAAE,CAAC;YACZ,GAAG,CAAC,IAAI,GAAG,OAAO,CAAC;QACrB,CAAC;IACH,CAAC;IACD,IAAI,EAAE,CAAC;AACT,CAAC;AAED,SAAgB,UAAU,CAAC,SAAiB,EAAE;IAC5C,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACvD,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,SAAS,GAAG,MAAM,EAAE,CAAC;AAC9D,CAAC;AAED,SAAgB,mBAAmB;IACjC,OAAO,IAAI,CAAC,GAAG,EAAE,CAAC;AACpB,CAAC"}
|
||||
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/utils/auth.ts"],"names":[],"mappings":";;;;;AAUA,sCAEC;AAED,kCAMC;AAED,wCAuCC;AAED,wDAWC;AAED,gCAIC;AAED,kDAEC;AAnFD,gEAA+B;AAC/B,8BAA2B;AAC3B,yCAAqC;AACrC,6CAAiC;AAGjC,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,gDAAgD,CAAC;AAC9F,MAAM,cAAc,GAAG,IAAI,CAAC;AAE5B,SAAgB,aAAa,CAAC,OAAoB;IAChD,OAAO,sBAAG,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,SAAS,EAAE,cAAc,EAAE,CAAC,CAAC;AACtE,CAAC;AAED,SAAgB,WAAW,CAAC,KAAa;IACvC,IAAI,CAAC;QACH,OAAO,sBAAG,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU,CAAgB,CAAC;IACtD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAEM,KAAK,UAAU,cAAc,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IAClF,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;IAE7C,IAAI,CAAC,UAAU,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACrD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YACnB,KAAK,EAAE;gBACL,IAAI,EAAE,cAAc;gBACpB,OAAO,EAAE,yCAAyC;aACnD;SACF,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAClC,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAEnC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YACnB,KAAK,EAAE;gBACL,IAAI,EAAE,cAAc;gBACpB,OAAO,EAAE,0BAA0B;aACpC;SACF,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,OAAE,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,cAAK,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,cAAK,CAAC,CAAC,KAAK,CAAC,IAAA,gBAAE,EAAC,cAAK,CAAC,EAAE,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACxG,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YACnB,KAAK,EAAE;gBACL,IAAI,EAAE,cAAc;gBACpB,OAAO,EAAE,4CAA4C;aACtD;SACF,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,GAAG,CAAC,IAAI,GAAG,OAAO,CAAC;IACnB,IAAI,EAAE,CAAC;AACT,CAAC;AAED,SAAgB,sBAAsB,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IACpF,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;IAE7C,IAAI,UAAU,IAAI,UAAU,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACnD,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAClC,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;QACnC,IAAI,OAAO,EAAE,CAAC;YACZ,GAAG,CAAC,IAAI,GAAG,OAAO,CAAC;QACrB,CAAC;IACH,CAAC;IACD,IAAI,EAAE,CAAC;AACT,CAAC;AAED,SAAgB,UAAU,CAAC,SAAiB,EAAE;IAC5C,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACvD,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,SAAS,GAAG,MAAM,EAAE,CAAC;AAC9D,CAAC;AAED,SAAgB,mBAAmB;IACjC,OAAO,IAAI,CAAC,GAAG,EAAE,CAAC;AACpB,CAAC"}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
export declare function canCompleteTask(dueDate: number): boolean;
|
||||
export declare const categoryCreateSchema: z.ZodObject<{
|
||||
name: z.ZodString;
|
||||
color: z.ZodString;
|
||||
@@ -26,20 +27,25 @@ export declare const categoryUpdateSchema: z.ZodObject<{
|
||||
order?: number | undefined;
|
||||
}>;
|
||||
export declare const repeatSchema: z.ZodEnum<["none", "daily", "weekly", "monthly", "custom"]>;
|
||||
export declare const repeatFieldSchema: z.ZodOptional<z.ZodEffects<z.ZodUnion<[z.ZodEnum<["none", "daily", "weekly", "monthly", "custom"]>, z.ZodLiteral<"">]>, "custom" | "none" | "daily" | "weekly" | "monthly", "" | "custom" | "none" | "daily" | "weekly" | "monthly">>;
|
||||
export declare const repeatIntervalFieldSchema: z.ZodOptional<z.ZodEffects<z.ZodNumber, number, number>>;
|
||||
export declare const taskCreateSchema: z.ZodObject<{
|
||||
title: z.ZodString;
|
||||
description: z.ZodOptional<z.ZodString>;
|
||||
categoryId: z.ZodString;
|
||||
categoryId: z.ZodNullable<z.ZodEffects<z.ZodString, string | null, string>>;
|
||||
priority: z.ZodOptional<z.ZodEnum<["none", "low", "medium", "high", "critical"]>>;
|
||||
dueDate: z.ZodOptional<z.ZodNumber>;
|
||||
dueTime: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
||||
dueTime: z.ZodEffects<z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>, string, string | undefined>;
|
||||
endTime: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
||||
repeat: z.ZodOptional<z.ZodEnum<["none", "daily", "weekly", "monthly", "custom"]>>;
|
||||
repeatInterval: z.ZodOptional<z.ZodNumber>;
|
||||
allDay: z.ZodOptional<z.ZodBoolean>;
|
||||
repeat: z.ZodOptional<z.ZodEffects<z.ZodUnion<[z.ZodEnum<["none", "daily", "weekly", "monthly", "custom"]>, z.ZodLiteral<"">]>, "custom" | "none" | "daily" | "weekly" | "monthly", "" | "custom" | "none" | "daily" | "weekly" | "monthly">>;
|
||||
repeatInterval: z.ZodOptional<z.ZodEffects<z.ZodNumber, number, number>>;
|
||||
repeatDays: z.ZodOptional<z.ZodString>;
|
||||
seriesId: z.ZodOptional<z.ZodString>;
|
||||
reminder: z.ZodOptional<z.ZodEnum<["none", "at_time", "15", "30", "60", "120", "1440"]>>;
|
||||
reminders: z.ZodOptional<z.ZodString>;
|
||||
assigneeId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
completedAt: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
||||
subtasks: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
title: z.ZodString;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
@@ -48,93 +54,189 @@ export declare const taskCreateSchema: z.ZodObject<{
|
||||
title: string;
|
||||
}>, "many">>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
categoryId: string;
|
||||
categoryId: string | null;
|
||||
title: string;
|
||||
dueTime: string;
|
||||
subtasks?: {
|
||||
title: string;
|
||||
}[] | undefined;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
dueDate?: number | undefined;
|
||||
dueTime?: string | null | undefined;
|
||||
endTime?: string | undefined;
|
||||
allDay?: boolean | undefined;
|
||||
repeat?: "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
seriesId?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminder?: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440" | undefined;
|
||||
subtasks?: {
|
||||
title: string;
|
||||
}[] | undefined;
|
||||
reminders?: string | undefined;
|
||||
completedAt?: number | null | undefined;
|
||||
}, {
|
||||
categoryId: string;
|
||||
categoryId: string | null;
|
||||
title: string;
|
||||
subtasks?: {
|
||||
title: string;
|
||||
}[] | undefined;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
dueDate?: number | undefined;
|
||||
dueTime?: string | null | undefined;
|
||||
dueTime?: string | undefined;
|
||||
endTime?: string | undefined;
|
||||
repeat?: "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
allDay?: boolean | undefined;
|
||||
repeat?: "" | "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
seriesId?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminder?: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440" | undefined;
|
||||
subtasks?: {
|
||||
title: string;
|
||||
}[] | undefined;
|
||||
reminders?: string | undefined;
|
||||
completedAt?: number | null | undefined;
|
||||
}>;
|
||||
export declare const taskUpdateSchema: z.ZodObject<{
|
||||
title: z.ZodOptional<z.ZodString>;
|
||||
description: z.ZodOptional<z.ZodString>;
|
||||
categoryId: z.ZodOptional<z.ZodString>;
|
||||
categoryId: z.ZodEffects<z.ZodOptional<z.ZodNullable<z.ZodString>>, string | null | undefined, string | null | undefined>;
|
||||
priority: z.ZodOptional<z.ZodEnum<["none", "low", "medium", "high", "critical"]>>;
|
||||
completed: z.ZodOptional<z.ZodBoolean>;
|
||||
dueDate: z.ZodOptional<z.ZodNumber>;
|
||||
dueTime: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
||||
dueTime: z.ZodEffects<z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>, string | undefined, string | undefined>;
|
||||
endTime: z.ZodOptional<z.ZodString>;
|
||||
reminders: z.ZodOptional<z.ZodString>;
|
||||
assigneeId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
completedAt: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
categoryId?: string | undefined;
|
||||
categoryId?: string | null | undefined;
|
||||
title?: string | undefined;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
completed?: boolean | undefined;
|
||||
dueDate?: number | undefined;
|
||||
dueTime?: string | null | undefined;
|
||||
dueTime?: string | undefined;
|
||||
endTime?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminders?: string | undefined;
|
||||
completedAt?: number | null | undefined;
|
||||
}, {
|
||||
categoryId?: string | undefined;
|
||||
categoryId?: string | null | undefined;
|
||||
title?: string | undefined;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
completed?: boolean | undefined;
|
||||
dueDate?: number | undefined;
|
||||
dueTime?: string | null | undefined;
|
||||
dueTime?: string | undefined;
|
||||
endTime?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminders?: string | undefined;
|
||||
completedAt?: number | null | undefined;
|
||||
}>;
|
||||
export declare const subtaskCreateSchema: z.ZodObject<{
|
||||
title: z.ZodString;
|
||||
description: z.ZodOptional<z.ZodString>;
|
||||
priority: z.ZodOptional<z.ZodEnum<["none", "low", "medium", "high", "critical"]>>;
|
||||
dueDate: z.ZodOptional<z.ZodNumber>;
|
||||
dueTime: z.ZodEffects<z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>, string, string | undefined>;
|
||||
endTime: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
||||
allDay: z.ZodOptional<z.ZodBoolean>;
|
||||
repeat: z.ZodOptional<z.ZodEffects<z.ZodUnion<[z.ZodEnum<["none", "daily", "weekly", "monthly", "custom"]>, z.ZodLiteral<"">]>, "custom" | "none" | "daily" | "weekly" | "monthly", "" | "custom" | "none" | "daily" | "weekly" | "monthly">>;
|
||||
repeatInterval: z.ZodOptional<z.ZodEffects<z.ZodNumber, number, number>>;
|
||||
repeatDays: z.ZodOptional<z.ZodString>;
|
||||
seriesId: z.ZodOptional<z.ZodString>;
|
||||
reminder: z.ZodOptional<z.ZodEnum<["none", "at_time", "15", "30", "60", "120", "1440"]>>;
|
||||
reminders: z.ZodOptional<z.ZodString>;
|
||||
assigneeId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
order: z.ZodOptional<z.ZodNumber>;
|
||||
parentSubtaskId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
title: string;
|
||||
dueTime: string;
|
||||
order?: number | undefined;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
dueDate?: number | undefined;
|
||||
endTime?: string | undefined;
|
||||
allDay?: boolean | undefined;
|
||||
repeat?: "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
seriesId?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminder?: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440" | undefined;
|
||||
reminders?: string | undefined;
|
||||
parentSubtaskId?: string | null | undefined;
|
||||
}, {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
dueDate?: number | undefined;
|
||||
dueTime?: string | undefined;
|
||||
endTime?: string | undefined;
|
||||
allDay?: boolean | undefined;
|
||||
repeat?: "" | "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
seriesId?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminder?: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440" | undefined;
|
||||
reminders?: string | undefined;
|
||||
parentSubtaskId?: string | null | undefined;
|
||||
}>;
|
||||
export declare const subtaskUpdateSchema: z.ZodObject<{
|
||||
title: z.ZodOptional<z.ZodString>;
|
||||
description: z.ZodOptional<z.ZodString>;
|
||||
priority: z.ZodOptional<z.ZodEnum<["none", "low", "medium", "high", "critical"]>>;
|
||||
completed: z.ZodOptional<z.ZodBoolean>;
|
||||
dueDate: z.ZodOptional<z.ZodNumber>;
|
||||
dueTime: z.ZodEffects<z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>, string | undefined, string | undefined>;
|
||||
endTime: z.ZodOptional<z.ZodString>;
|
||||
allDay: z.ZodOptional<z.ZodBoolean>;
|
||||
repeat: z.ZodOptional<z.ZodEffects<z.ZodUnion<[z.ZodEnum<["none", "daily", "weekly", "monthly", "custom"]>, z.ZodLiteral<"">]>, "custom" | "none" | "daily" | "weekly" | "monthly", "" | "custom" | "none" | "daily" | "weekly" | "monthly">>;
|
||||
repeatInterval: z.ZodOptional<z.ZodEffects<z.ZodNumber, number, number>>;
|
||||
repeatDays: z.ZodOptional<z.ZodString>;
|
||||
seriesId: z.ZodOptional<z.ZodString>;
|
||||
reminder: z.ZodOptional<z.ZodEnum<["none", "at_time", "15", "30", "60", "120", "1440"]>>;
|
||||
reminders: z.ZodOptional<z.ZodString>;
|
||||
assigneeId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
order: z.ZodOptional<z.ZodNumber>;
|
||||
parentSubtaskId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
order?: number | undefined;
|
||||
title?: string | undefined;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
completed?: boolean | undefined;
|
||||
dueDate?: number | undefined;
|
||||
dueTime?: string | undefined;
|
||||
endTime?: string | undefined;
|
||||
allDay?: boolean | undefined;
|
||||
repeat?: "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
seriesId?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminder?: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440" | undefined;
|
||||
reminders?: string | undefined;
|
||||
parentSubtaskId?: string | null | undefined;
|
||||
}, {
|
||||
order?: number | undefined;
|
||||
title?: string | undefined;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
completed?: boolean | undefined;
|
||||
dueDate?: number | undefined;
|
||||
dueTime?: string | undefined;
|
||||
endTime?: string | undefined;
|
||||
allDay?: boolean | undefined;
|
||||
repeat?: "" | "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
seriesId?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminder?: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440" | undefined;
|
||||
reminders?: string | undefined;
|
||||
parentSubtaskId?: string | null | undefined;
|
||||
}>;
|
||||
export declare const userSettingsSchema: z.ZodObject<{
|
||||
darkMode: z.ZodOptional<z.ZodBoolean>;
|
||||
@@ -174,6 +276,29 @@ export declare const repeatProfileSchema: z.ZodObject<{
|
||||
repeatInterval: number;
|
||||
repeatDays: string;
|
||||
}>;
|
||||
export declare const repeatProfileUpdateSchema: z.ZodObject<{
|
||||
name: z.ZodOptional<z.ZodString>;
|
||||
repeat: z.ZodOptional<z.ZodEnum<["none", "daily", "weekly", "monthly", "custom"]>>;
|
||||
repeatInterval: z.ZodOptional<z.ZodNumber>;
|
||||
repeatDays: z.ZodOptional<z.ZodString>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
name?: string | undefined;
|
||||
repeat?: "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
}, {
|
||||
name?: string | undefined;
|
||||
repeat?: "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
}>;
|
||||
export declare const searchQuerySchema: z.ZodObject<{
|
||||
q: z.ZodString;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
q: string;
|
||||
}, {
|
||||
q: string;
|
||||
}>;
|
||||
export declare const friendRequestSchema: z.ZodObject<{
|
||||
username: z.ZodString;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
@@ -203,6 +328,19 @@ export declare const friendshipSchema: z.ZodObject<{
|
||||
status: "pending" | "accepted";
|
||||
updatedAt: number;
|
||||
}>;
|
||||
export declare const tombstoneSchema: z.ZodObject<{
|
||||
entity: z.ZodEnum<["categories", "tasks", "subtasks", "repeatProfiles", "friendships"]>;
|
||||
id: z.ZodString;
|
||||
updatedAt: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
id: string;
|
||||
updatedAt: number;
|
||||
entity: "categories" | "tasks" | "subtasks" | "repeatProfiles" | "friendships";
|
||||
}, {
|
||||
id: string;
|
||||
updatedAt: number;
|
||||
entity: "categories" | "tasks" | "subtasks" | "repeatProfiles" | "friendships";
|
||||
}>;
|
||||
export declare const pushChangesSchema: z.ZodObject<{
|
||||
changes: z.ZodObject<{
|
||||
categories: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
@@ -231,16 +369,18 @@ export declare const pushChangesSchema: z.ZodObject<{
|
||||
tasks: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
title: z.ZodString;
|
||||
description: z.ZodOptional<z.ZodString>;
|
||||
categoryId: z.ZodString;
|
||||
categoryId: z.ZodNullable<z.ZodEffects<z.ZodString, string | null, string>>;
|
||||
priority: z.ZodOptional<z.ZodEnum<["none", "low", "medium", "high", "critical"]>>;
|
||||
dueDate: z.ZodOptional<z.ZodNumber>;
|
||||
dueTime: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
||||
dueTime: z.ZodEffects<z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>, string, string | undefined>;
|
||||
endTime: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
||||
repeat: z.ZodOptional<z.ZodEnum<["none", "daily", "weekly", "monthly", "custom"]>>;
|
||||
repeatInterval: z.ZodOptional<z.ZodNumber>;
|
||||
allDay: z.ZodOptional<z.ZodBoolean>;
|
||||
repeat: z.ZodOptional<z.ZodEffects<z.ZodUnion<[z.ZodEnum<["none", "daily", "weekly", "monthly", "custom"]>, z.ZodLiteral<"">]>, "custom" | "none" | "daily" | "weekly" | "monthly", "" | "custom" | "none" | "daily" | "weekly" | "monthly">>;
|
||||
repeatInterval: z.ZodOptional<z.ZodEffects<z.ZodNumber, number, number>>;
|
||||
repeatDays: z.ZodOptional<z.ZodString>;
|
||||
seriesId: z.ZodOptional<z.ZodString>;
|
||||
reminder: z.ZodOptional<z.ZodEnum<["none", "at_time", "15", "30", "60", "120", "1440"]>>;
|
||||
reminders: z.ZodOptional<z.ZodString>;
|
||||
assigneeId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
subtasks: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
title: z.ZodString;
|
||||
@@ -254,52 +394,73 @@ export declare const pushChangesSchema: z.ZodObject<{
|
||||
completed: z.ZodBoolean;
|
||||
createdAt: z.ZodNumber;
|
||||
updatedAt: z.ZodNumber;
|
||||
completedAt: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
categoryId: string;
|
||||
categoryId: string | null;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
dueTime: string;
|
||||
subtasks?: {
|
||||
title: string;
|
||||
}[] | undefined;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
dueDate?: number | undefined;
|
||||
dueTime?: string | null | undefined;
|
||||
endTime?: string | undefined;
|
||||
allDay?: boolean | undefined;
|
||||
repeat?: "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
seriesId?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminder?: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440" | undefined;
|
||||
subtasks?: {
|
||||
title: string;
|
||||
}[] | undefined;
|
||||
reminders?: string | undefined;
|
||||
completedAt?: number | null | undefined;
|
||||
}, {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
categoryId: string;
|
||||
categoryId: string | null;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
subtasks?: {
|
||||
title: string;
|
||||
}[] | undefined;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
dueDate?: number | undefined;
|
||||
dueTime?: string | null | undefined;
|
||||
dueTime?: string | undefined;
|
||||
endTime?: string | undefined;
|
||||
repeat?: "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
allDay?: boolean | undefined;
|
||||
repeat?: "" | "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
seriesId?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminder?: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440" | undefined;
|
||||
subtasks?: {
|
||||
title: string;
|
||||
}[] | undefined;
|
||||
reminders?: string | undefined;
|
||||
completedAt?: number | null | undefined;
|
||||
}>, "many">>;
|
||||
subtasks: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
title: z.ZodString;
|
||||
description: z.ZodOptional<z.ZodString>;
|
||||
priority: z.ZodOptional<z.ZodEnum<["none", "low", "medium", "high", "critical"]>>;
|
||||
dueDate: z.ZodOptional<z.ZodNumber>;
|
||||
dueTime: z.ZodEffects<z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>, string, string | undefined>;
|
||||
endTime: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
||||
allDay: z.ZodOptional<z.ZodBoolean>;
|
||||
repeat: z.ZodOptional<z.ZodEffects<z.ZodUnion<[z.ZodEnum<["none", "daily", "weekly", "monthly", "custom"]>, z.ZodLiteral<"">]>, "custom" | "none" | "daily" | "weekly" | "monthly", "" | "custom" | "none" | "daily" | "weekly" | "monthly">>;
|
||||
repeatInterval: z.ZodOptional<z.ZodEffects<z.ZodNumber, number, number>>;
|
||||
repeatDays: z.ZodOptional<z.ZodString>;
|
||||
seriesId: z.ZodOptional<z.ZodString>;
|
||||
reminder: z.ZodOptional<z.ZodEnum<["none", "at_time", "15", "30", "60", "120", "1440"]>>;
|
||||
reminders: z.ZodOptional<z.ZodString>;
|
||||
assigneeId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
order: z.ZodOptional<z.ZodNumber>;
|
||||
parentSubtaskId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
} & {
|
||||
id: z.ZodString;
|
||||
taskId: z.ZodString;
|
||||
@@ -312,8 +473,22 @@ export declare const pushChangesSchema: z.ZodObject<{
|
||||
updatedAt: number;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
dueTime: string;
|
||||
taskId: string;
|
||||
order?: number | undefined;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
dueDate?: number | undefined;
|
||||
endTime?: string | undefined;
|
||||
allDay?: boolean | undefined;
|
||||
repeat?: "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
seriesId?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminder?: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440" | undefined;
|
||||
reminders?: string | undefined;
|
||||
parentSubtaskId?: string | null | undefined;
|
||||
}, {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
@@ -322,6 +497,20 @@ export declare const pushChangesSchema: z.ZodObject<{
|
||||
completed: boolean;
|
||||
taskId: string;
|
||||
order?: number | undefined;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
dueDate?: number | undefined;
|
||||
dueTime?: string | undefined;
|
||||
endTime?: string | undefined;
|
||||
allDay?: boolean | undefined;
|
||||
repeat?: "" | "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
seriesId?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminder?: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440" | undefined;
|
||||
reminders?: string | undefined;
|
||||
parentSubtaskId?: string | null | undefined;
|
||||
}>, "many">>;
|
||||
repeatProfiles: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
name: z.ZodString;
|
||||
@@ -372,14 +561,6 @@ export declare const pushChangesSchema: z.ZodObject<{
|
||||
updatedAt: number;
|
||||
}>, "many">>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
friendships?: {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
userId: string;
|
||||
friendId: string;
|
||||
status: "pending" | "accepted";
|
||||
updatedAt: number;
|
||||
}[] | undefined;
|
||||
categories?: {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -392,23 +573,26 @@ export declare const pushChangesSchema: z.ZodObject<{
|
||||
id: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
categoryId: string;
|
||||
categoryId: string | null;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
dueTime: string;
|
||||
subtasks?: {
|
||||
title: string;
|
||||
}[] | undefined;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
dueDate?: number | undefined;
|
||||
dueTime?: string | null | undefined;
|
||||
endTime?: string | undefined;
|
||||
allDay?: boolean | undefined;
|
||||
repeat?: "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
seriesId?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminder?: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440" | undefined;
|
||||
subtasks?: {
|
||||
title: string;
|
||||
}[] | undefined;
|
||||
reminders?: string | undefined;
|
||||
completedAt?: number | null | undefined;
|
||||
}[] | undefined;
|
||||
subtasks?: {
|
||||
id: string;
|
||||
@@ -416,8 +600,22 @@ export declare const pushChangesSchema: z.ZodObject<{
|
||||
updatedAt: number;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
dueTime: string;
|
||||
taskId: string;
|
||||
order?: number | undefined;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
dueDate?: number | undefined;
|
||||
endTime?: string | undefined;
|
||||
allDay?: boolean | undefined;
|
||||
repeat?: "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
seriesId?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminder?: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440" | undefined;
|
||||
reminders?: string | undefined;
|
||||
parentSubtaskId?: string | null | undefined;
|
||||
}[] | undefined;
|
||||
repeatProfiles?: {
|
||||
id: string;
|
||||
@@ -428,15 +626,15 @@ export declare const pushChangesSchema: z.ZodObject<{
|
||||
repeatInterval: number;
|
||||
repeatDays: string;
|
||||
}[] | undefined;
|
||||
friendships?: {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
userId: string;
|
||||
friendId: string;
|
||||
status: "pending" | "accepted";
|
||||
updatedAt: number;
|
||||
}[] | undefined;
|
||||
}, {
|
||||
friendships?: {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
userId: string;
|
||||
friendId: string;
|
||||
status: "pending" | "accepted";
|
||||
updatedAt: number;
|
||||
}[] | undefined;
|
||||
categories?: {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -449,23 +647,26 @@ export declare const pushChangesSchema: z.ZodObject<{
|
||||
id: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
categoryId: string;
|
||||
categoryId: string | null;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
subtasks?: {
|
||||
title: string;
|
||||
}[] | undefined;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
dueDate?: number | undefined;
|
||||
dueTime?: string | null | undefined;
|
||||
dueTime?: string | undefined;
|
||||
endTime?: string | undefined;
|
||||
repeat?: "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
allDay?: boolean | undefined;
|
||||
repeat?: "" | "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
seriesId?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminder?: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440" | undefined;
|
||||
subtasks?: {
|
||||
title: string;
|
||||
}[] | undefined;
|
||||
reminders?: string | undefined;
|
||||
completedAt?: number | null | undefined;
|
||||
}[] | undefined;
|
||||
subtasks?: {
|
||||
id: string;
|
||||
@@ -475,6 +676,20 @@ export declare const pushChangesSchema: z.ZodObject<{
|
||||
completed: boolean;
|
||||
taskId: string;
|
||||
order?: number | undefined;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
dueDate?: number | undefined;
|
||||
dueTime?: string | undefined;
|
||||
endTime?: string | undefined;
|
||||
allDay?: boolean | undefined;
|
||||
repeat?: "" | "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
seriesId?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminder?: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440" | undefined;
|
||||
reminders?: string | undefined;
|
||||
parentSubtaskId?: string | null | undefined;
|
||||
}[] | undefined;
|
||||
repeatProfiles?: {
|
||||
id: string;
|
||||
@@ -485,18 +700,31 @@ export declare const pushChangesSchema: z.ZodObject<{
|
||||
repeatInterval: number;
|
||||
repeatDays: string;
|
||||
}[] | undefined;
|
||||
friendships?: {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
userId: string;
|
||||
friendId: string;
|
||||
status: "pending" | "accepted";
|
||||
updatedAt: number;
|
||||
}[] | undefined;
|
||||
}>;
|
||||
deleted: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
entity: z.ZodEnum<["categories", "tasks", "subtasks", "repeatProfiles", "friendships"]>;
|
||||
id: z.ZodString;
|
||||
updatedAt: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
id: string;
|
||||
updatedAt: number;
|
||||
entity: "categories" | "tasks" | "subtasks" | "repeatProfiles" | "friendships";
|
||||
}, {
|
||||
id: string;
|
||||
updatedAt: number;
|
||||
entity: "categories" | "tasks" | "subtasks" | "repeatProfiles" | "friendships";
|
||||
}>, "many">>;
|
||||
lastPulledAt: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
changes: {
|
||||
friendships?: {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
userId: string;
|
||||
friendId: string;
|
||||
status: "pending" | "accepted";
|
||||
updatedAt: number;
|
||||
}[] | undefined;
|
||||
categories?: {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -509,23 +737,26 @@ export declare const pushChangesSchema: z.ZodObject<{
|
||||
id: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
categoryId: string;
|
||||
categoryId: string | null;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
dueTime: string;
|
||||
subtasks?: {
|
||||
title: string;
|
||||
}[] | undefined;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
dueDate?: number | undefined;
|
||||
dueTime?: string | null | undefined;
|
||||
endTime?: string | undefined;
|
||||
allDay?: boolean | undefined;
|
||||
repeat?: "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
seriesId?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminder?: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440" | undefined;
|
||||
subtasks?: {
|
||||
title: string;
|
||||
}[] | undefined;
|
||||
reminders?: string | undefined;
|
||||
completedAt?: number | null | undefined;
|
||||
}[] | undefined;
|
||||
subtasks?: {
|
||||
id: string;
|
||||
@@ -533,8 +764,22 @@ export declare const pushChangesSchema: z.ZodObject<{
|
||||
updatedAt: number;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
dueTime: string;
|
||||
taskId: string;
|
||||
order?: number | undefined;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
dueDate?: number | undefined;
|
||||
endTime?: string | undefined;
|
||||
allDay?: boolean | undefined;
|
||||
repeat?: "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
seriesId?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminder?: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440" | undefined;
|
||||
reminders?: string | undefined;
|
||||
parentSubtaskId?: string | null | undefined;
|
||||
}[] | undefined;
|
||||
repeatProfiles?: {
|
||||
id: string;
|
||||
@@ -545,18 +790,23 @@ export declare const pushChangesSchema: z.ZodObject<{
|
||||
repeatInterval: number;
|
||||
repeatDays: string;
|
||||
}[] | undefined;
|
||||
friendships?: {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
userId: string;
|
||||
friendId: string;
|
||||
status: "pending" | "accepted";
|
||||
updatedAt: number;
|
||||
}[] | undefined;
|
||||
};
|
||||
lastPulledAt: number;
|
||||
deleted?: {
|
||||
id: string;
|
||||
updatedAt: number;
|
||||
entity: "categories" | "tasks" | "subtasks" | "repeatProfiles" | "friendships";
|
||||
}[] | undefined;
|
||||
}, {
|
||||
changes: {
|
||||
friendships?: {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
userId: string;
|
||||
friendId: string;
|
||||
status: "pending" | "accepted";
|
||||
updatedAt: number;
|
||||
}[] | undefined;
|
||||
categories?: {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -569,23 +819,26 @@ export declare const pushChangesSchema: z.ZodObject<{
|
||||
id: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
categoryId: string;
|
||||
categoryId: string | null;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
subtasks?: {
|
||||
title: string;
|
||||
}[] | undefined;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
dueDate?: number | undefined;
|
||||
dueTime?: string | null | undefined;
|
||||
dueTime?: string | undefined;
|
||||
endTime?: string | undefined;
|
||||
repeat?: "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
allDay?: boolean | undefined;
|
||||
repeat?: "" | "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
seriesId?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminder?: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440" | undefined;
|
||||
subtasks?: {
|
||||
title: string;
|
||||
}[] | undefined;
|
||||
reminders?: string | undefined;
|
||||
completedAt?: number | null | undefined;
|
||||
}[] | undefined;
|
||||
subtasks?: {
|
||||
id: string;
|
||||
@@ -595,6 +848,20 @@ export declare const pushChangesSchema: z.ZodObject<{
|
||||
completed: boolean;
|
||||
taskId: string;
|
||||
order?: number | undefined;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
dueDate?: number | undefined;
|
||||
dueTime?: string | undefined;
|
||||
endTime?: string | undefined;
|
||||
allDay?: boolean | undefined;
|
||||
repeat?: "" | "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
seriesId?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminder?: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440" | undefined;
|
||||
reminders?: string | undefined;
|
||||
parentSubtaskId?: string | null | undefined;
|
||||
}[] | undefined;
|
||||
repeatProfiles?: {
|
||||
id: string;
|
||||
@@ -605,8 +872,21 @@ export declare const pushChangesSchema: z.ZodObject<{
|
||||
repeatInterval: number;
|
||||
repeatDays: string;
|
||||
}[] | undefined;
|
||||
friendships?: {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
userId: string;
|
||||
friendId: string;
|
||||
status: "pending" | "accepted";
|
||||
updatedAt: number;
|
||||
}[] | undefined;
|
||||
};
|
||||
lastPulledAt: number;
|
||||
deleted?: {
|
||||
id: string;
|
||||
updatedAt: number;
|
||||
entity: "categories" | "tasks" | "subtasks" | "repeatProfiles" | "friendships";
|
||||
}[] | undefined;
|
||||
}>;
|
||||
export declare const syncQuerySchema: z.ZodObject<{
|
||||
since: z.ZodPipeline<z.ZodEffects<z.ZodString, number, string>, z.ZodNumber>;
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,oBAAoB;;;;;;;;;;;;EAI/B,CAAC;AAEH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;EAI/B,CAAC;AAEH,eAAO,MAAM,YAAY,6DAA2D,CAAC;AAErF,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAe3B,CAAC;AAEH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAU3B,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;EAG9B,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;EAI9B,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;EAO7B,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;EAK9B,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;EAE9B,CAAC;AAEH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;EAO3B,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAS5B,CAAC;AAEH,eAAO,MAAM,eAAe;;;;;;EAE1B,CAAC;AAEH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAU1B,CAAC"}
|
||||
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAKxD;AAED,eAAO,MAAM,oBAAoB;;;;;;;;;;;;EAI/B,CAAC;AAEH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;EAI/B,CAAC;AAEH,eAAO,MAAM,YAAY,6DAA2D,CAAC;AAIrF,eAAO,MAAM,iBAAiB,uOAGjB,CAAC;AACd,eAAO,MAAM,yBAAyB,0DAMzB,CAAC;AAEd,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkB3B,CAAC;AAEH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAY3B,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiB9B,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkB9B,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;EAO7B,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;EAK9B,CAAC;AAEH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;EAKpC,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;EAE5B,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;EAE9B,CAAC;AAEH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;EAO3B,CAAC;AAEH,eAAO,MAAM,eAAe;;;;;;;;;;;;EAI1B,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAU5B,CAAC;AAEH,eAAO,MAAM,eAAe;;;;;;EAE1B,CAAC;AAEH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAU1B,CAAC"}
|
||||
@@ -1,7 +1,15 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.taskQuerySchema = exports.syncQuerySchema = exports.pushChangesSchema = exports.friendshipSchema = exports.friendRequestSchema = exports.repeatProfileSchema = exports.userSettingsSchema = exports.subtaskUpdateSchema = exports.subtaskCreateSchema = exports.taskUpdateSchema = exports.taskCreateSchema = exports.repeatSchema = exports.categoryUpdateSchema = exports.categoryCreateSchema = void 0;
|
||||
exports.taskQuerySchema = exports.syncQuerySchema = exports.pushChangesSchema = exports.tombstoneSchema = exports.friendshipSchema = exports.friendRequestSchema = exports.searchQuerySchema = exports.repeatProfileUpdateSchema = exports.repeatProfileSchema = exports.userSettingsSchema = exports.subtaskUpdateSchema = exports.subtaskCreateSchema = exports.taskUpdateSchema = exports.taskCreateSchema = exports.repeatIntervalFieldSchema = exports.repeatFieldSchema = exports.repeatSchema = exports.categoryUpdateSchema = exports.categoryCreateSchema = void 0;
|
||||
exports.canCompleteTask = canCompleteTask;
|
||||
const zod_1 = require("zod");
|
||||
function canCompleteTask(dueDate) {
|
||||
if (dueDate === 0)
|
||||
return true;
|
||||
const now = new Date();
|
||||
const endOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999).getTime();
|
||||
return dueDate <= endOfToday;
|
||||
}
|
||||
exports.categoryCreateSchema = zod_1.z.object({
|
||||
name: zod_1.z.string().min(1).max(50),
|
||||
color: zod_1.z.string().regex(/^#[0-9A-Fa-f]{6}$/),
|
||||
@@ -13,41 +21,87 @@ exports.categoryUpdateSchema = zod_1.z.object({
|
||||
order: zod_1.z.number().int().min(0).optional(),
|
||||
});
|
||||
exports.repeatSchema = zod_1.z.enum(['none', 'daily', 'weekly', 'monthly', 'custom']);
|
||||
// The client stores '' / 0 for unset repeat values; normalize them so stale or
|
||||
// legacy rows never fail validation on push.
|
||||
exports.repeatFieldSchema = exports.repeatSchema
|
||||
.or(zod_1.z.literal(''))
|
||||
.transform((v) => (v === '' ? 'none' : v))
|
||||
.optional();
|
||||
exports.repeatIntervalFieldSchema = zod_1.z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(30)
|
||||
.transform((v) => Math.max(1, v))
|
||||
.optional();
|
||||
exports.taskCreateSchema = zod_1.z.object({
|
||||
title: zod_1.z.string().min(1).max(100),
|
||||
description: zod_1.z.string().max(1000).optional(),
|
||||
categoryId: zod_1.z.string().min(1),
|
||||
categoryId: zod_1.z.string().max(100).transform((v) => (v === '' ? null : v)).nullable(),
|
||||
priority: zod_1.z.enum(['none', 'low', 'medium', 'high', 'critical']).optional(),
|
||||
dueDate: zod_1.z.number().int().min(0).optional(),
|
||||
dueTime: zod_1.z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().nullable(),
|
||||
dueTime: zod_1.z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().or(zod_1.z.literal('')).transform(v => v ?? ''),
|
||||
endTime: zod_1.z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().or(zod_1.z.literal('')),
|
||||
repeat: exports.repeatSchema.optional(),
|
||||
repeatInterval: zod_1.z.number().int().min(1).max(30).optional(),
|
||||
allDay: zod_1.z.boolean().optional(),
|
||||
repeat: exports.repeatFieldSchema,
|
||||
repeatInterval: exports.repeatIntervalFieldSchema,
|
||||
repeatDays: zod_1.z.string().max(20).optional(),
|
||||
seriesId: zod_1.z.string().max(50).optional(),
|
||||
reminder: zod_1.z.enum(['none', 'at_time', '15', '30', '60', '120', '1440']).optional(),
|
||||
reminders: zod_1.z.string().max(100).optional(),
|
||||
assigneeId: zod_1.z.string().nullable().optional(),
|
||||
completedAt: zod_1.z.number().int().min(0).nullable().optional(),
|
||||
subtasks: zod_1.z.array(zod_1.z.object({ title: zod_1.z.string().min(1).max(100) })).optional(),
|
||||
});
|
||||
exports.taskUpdateSchema = zod_1.z.object({
|
||||
title: zod_1.z.string().min(1).max(100).optional(),
|
||||
description: zod_1.z.string().max(1000).optional(),
|
||||
categoryId: zod_1.z.string().min(1).optional(),
|
||||
categoryId: zod_1.z.string().max(100).nullable().optional().transform((v) => (v === '' ? null : v)),
|
||||
priority: zod_1.z.enum(['none', 'low', 'medium', 'high', 'critical']).optional(),
|
||||
completed: zod_1.z.boolean().optional(),
|
||||
dueDate: zod_1.z.number().int().min(0).optional(),
|
||||
dueTime: zod_1.z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().nullable(),
|
||||
dueTime: zod_1.z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().or(zod_1.z.literal('')).transform(v => v === '' ? undefined : v),
|
||||
endTime: zod_1.z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional(),
|
||||
reminders: zod_1.z.string().max(100).optional(),
|
||||
assigneeId: zod_1.z.string().nullable().optional(),
|
||||
completedAt: zod_1.z.number().int().min(0).nullable().optional(),
|
||||
});
|
||||
exports.subtaskCreateSchema = zod_1.z.object({
|
||||
title: zod_1.z.string().min(1).max(100),
|
||||
description: zod_1.z.string().max(1000).optional(),
|
||||
priority: zod_1.z.enum(['none', 'low', 'medium', 'high', 'critical']).optional(),
|
||||
dueDate: zod_1.z.number().int().min(0).optional(),
|
||||
dueTime: zod_1.z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().or(zod_1.z.literal('')).transform(v => v ?? ''),
|
||||
endTime: zod_1.z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().or(zod_1.z.literal('')),
|
||||
allDay: zod_1.z.boolean().optional(),
|
||||
repeat: exports.repeatFieldSchema,
|
||||
repeatInterval: exports.repeatIntervalFieldSchema,
|
||||
repeatDays: zod_1.z.string().max(20).optional(),
|
||||
seriesId: zod_1.z.string().max(50).optional(),
|
||||
reminder: zod_1.z.enum(['none', 'at_time', '15', '30', '60', '120', '1440']).optional(),
|
||||
reminders: zod_1.z.string().max(100).optional(),
|
||||
assigneeId: zod_1.z.string().nullable().optional(),
|
||||
order: zod_1.z.number().int().min(0).optional(),
|
||||
parentSubtaskId: zod_1.z.string().nullable().optional(),
|
||||
});
|
||||
exports.subtaskUpdateSchema = zod_1.z.object({
|
||||
title: zod_1.z.string().min(1).max(100).optional(),
|
||||
description: zod_1.z.string().max(1000).optional(),
|
||||
priority: zod_1.z.enum(['none', 'low', 'medium', 'high', 'critical']).optional(),
|
||||
completed: zod_1.z.boolean().optional(),
|
||||
dueDate: zod_1.z.number().int().min(0).optional(),
|
||||
dueTime: zod_1.z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().or(zod_1.z.literal('')).transform(v => v === '' ? undefined : v),
|
||||
endTime: zod_1.z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional(),
|
||||
allDay: zod_1.z.boolean().optional(),
|
||||
repeat: exports.repeatFieldSchema,
|
||||
repeatInterval: exports.repeatIntervalFieldSchema,
|
||||
repeatDays: zod_1.z.string().max(20).optional(),
|
||||
seriesId: zod_1.z.string().max(50).optional(),
|
||||
reminder: zod_1.z.enum(['none', 'at_time', '15', '30', '60', '120', '1440']).optional(),
|
||||
reminders: zod_1.z.string().max(100).optional(),
|
||||
assigneeId: zod_1.z.string().nullable().optional(),
|
||||
order: zod_1.z.number().int().min(0).optional(),
|
||||
parentSubtaskId: zod_1.z.string().nullable().optional(),
|
||||
});
|
||||
exports.userSettingsSchema = zod_1.z.object({
|
||||
darkMode: zod_1.z.boolean().optional(),
|
||||
@@ -63,6 +117,15 @@ exports.repeatProfileSchema = zod_1.z.object({
|
||||
repeatInterval: zod_1.z.number().int().min(1).max(30),
|
||||
repeatDays: zod_1.z.string().max(20),
|
||||
});
|
||||
exports.repeatProfileUpdateSchema = zod_1.z.object({
|
||||
name: zod_1.z.string().min(1).max(50).optional(),
|
||||
repeat: exports.repeatSchema.optional(),
|
||||
repeatInterval: zod_1.z.number().int().min(1).max(30).optional(),
|
||||
repeatDays: zod_1.z.string().max(20).optional(),
|
||||
});
|
||||
exports.searchQuerySchema = zod_1.z.object({
|
||||
q: zod_1.z.string().min(1).max(50),
|
||||
});
|
||||
exports.friendRequestSchema = zod_1.z.object({
|
||||
username: zod_1.z.string().min(1).max(50),
|
||||
});
|
||||
@@ -74,14 +137,20 @@ exports.friendshipSchema = zod_1.z.object({
|
||||
createdAt: zod_1.z.number(),
|
||||
updatedAt: zod_1.z.number(),
|
||||
});
|
||||
exports.tombstoneSchema = zod_1.z.object({
|
||||
entity: zod_1.z.enum(['categories', 'tasks', 'subtasks', 'repeatProfiles', 'friendships']),
|
||||
id: zod_1.z.string(),
|
||||
updatedAt: zod_1.z.number().int().min(0),
|
||||
});
|
||||
exports.pushChangesSchema = zod_1.z.object({
|
||||
changes: zod_1.z.object({
|
||||
categories: zod_1.z.array(exports.categoryCreateSchema.extend({ id: zod_1.z.string(), createdAt: zod_1.z.number(), updatedAt: zod_1.z.number() })).optional(),
|
||||
tasks: zod_1.z.array(exports.taskCreateSchema.extend({ id: zod_1.z.string(), completed: zod_1.z.boolean(), createdAt: zod_1.z.number(), updatedAt: zod_1.z.number() })).optional(),
|
||||
tasks: zod_1.z.array(exports.taskCreateSchema.extend({ id: zod_1.z.string(), completed: zod_1.z.boolean(), createdAt: zod_1.z.number(), updatedAt: zod_1.z.number(), completedAt: zod_1.z.number().int().min(0).nullable().optional() })).optional(),
|
||||
subtasks: zod_1.z.array(exports.subtaskCreateSchema.extend({ id: zod_1.z.string(), taskId: zod_1.z.string(), completed: zod_1.z.boolean(), createdAt: zod_1.z.number(), updatedAt: zod_1.z.number() })).optional(),
|
||||
repeatProfiles: zod_1.z.array(exports.repeatProfileSchema.extend({ id: zod_1.z.string(), createdAt: zod_1.z.number(), updatedAt: zod_1.z.number() })).optional(),
|
||||
friendships: zod_1.z.array(exports.friendshipSchema).optional(),
|
||||
}),
|
||||
deleted: zod_1.z.array(exports.tombstoneSchema).optional(),
|
||||
lastPulledAt: zod_1.z.number().int().min(0),
|
||||
});
|
||||
exports.syncQuerySchema = zod_1.z.object({
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"name": "carry-your-live-backend",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@types/node-fetch": "^2.6.13",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
@@ -15,6 +16,7 @@
|
||||
"express": "^4.21.0",
|
||||
"express-rate-limit": "^8.6.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"node-fetch": "^3.3.2",
|
||||
"pg": "^8.13.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
@@ -1015,12 +1017,21 @@
|
||||
"version": "22.20.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz",
|
||||
"integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node-fetch": {
|
||||
"version": "2.6.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz",
|
||||
"integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"form-data": "^4.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/pg": {
|
||||
"version": "8.20.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.4.tgz",
|
||||
@@ -1099,6 +1110,12 @@
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bcryptjs": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||
@@ -1198,6 +1215,18 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
@@ -1251,6 +1280,15 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
@@ -1268,6 +1306,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
@@ -1504,6 +1551,21 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
|
||||
@@ -1651,6 +1713,29 @@
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fetch-blob": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-domexception": "^1.0.0",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20 || >= 14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||
@@ -1684,6 +1769,34 @@
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.4",
|
||||
"mime-types": "^2.1.35"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fetch-blob": "^3.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@@ -1800,6 +1913,21 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
@@ -2037,6 +2165,44 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"deprecated": "Use your platform's native DOMException instead",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"fetch-blob": "^3.1.4",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -2977,7 +3143,6 @@
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
@@ -3007,6 +3172,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"db:studio": "drizzle-kit studio"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node-fetch": "^2.6.13",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
@@ -20,6 +21,7 @@
|
||||
"express": "^4.21.0",
|
||||
"express-rate-limit": "^8.6.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"node-fetch": "^3.3.2",
|
||||
"pg": "^8.13.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { pgTable, text, integer, bigint, boolean, timestamp, unique, index } from 'drizzle-orm/pg-core';
|
||||
import { pgTable, text, integer, bigint, boolean, unique, index, primaryKey, AnyPgColumn } from 'drizzle-orm/pg-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
export const ENTITIES = ['categories', 'tasks', 'subtasks', 'repeatProfiles', 'friendships'] as const;
|
||||
export type EntityName = (typeof ENTITIES)[number];
|
||||
|
||||
export const users = pgTable('users', {
|
||||
id: text('id').primaryKey(),
|
||||
username: text('username').notNull().unique(),
|
||||
passwordHash: text('password_hash').notNull(),
|
||||
resetToken: text('reset_token'),
|
||||
resetTokenExpiry: bigint('reset_token_expiry', { mode: 'number' }),
|
||||
expoPushToken: text('expo_push_token'),
|
||||
pushTokenUpdatedAt: bigint('push_token_updated_at', { mode: 'number' }),
|
||||
createdAt: bigint('created_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
});
|
||||
|
||||
@@ -40,7 +45,8 @@ export const categories = pgTable('categories', {
|
||||
export const tasks = pgTable('tasks', {
|
||||
id: text('id').primaryKey(),
|
||||
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||
categoryId: text('category_id').notNull().references(() => categories.id, { onDelete: 'cascade' }),
|
||||
categoryId: text('category_id').references(() => categories.id, { onDelete: 'cascade' }),
|
||||
tags: text('tags').notNull().default(''),
|
||||
title: text('title').notNull(),
|
||||
description: text('description').notNull().default(''),
|
||||
priority: text('priority', { enum: ['none', 'low', 'medium', 'high', 'critical'] }).notNull().default('none'),
|
||||
@@ -56,26 +62,33 @@ export const tasks = pgTable('tasks', {
|
||||
assigneeId: text('assignee_id').references(() => users.id, { onDelete: 'set null' }),
|
||||
reminder: text('reminder', { enum: ['none', 'at_time', '15', '30', '60', '120', '1440'] }).notNull().default('none'),
|
||||
reminders: text('reminders').notNull().default(''),
|
||||
completedAt: bigint('completed_at', { mode: 'number' }),
|
||||
createdAt: bigint('created_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
updatedAt: bigint('updated_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
});
|
||||
|
||||
export const repeatProfiles = pgTable('repeat_profiles', {
|
||||
id: text('id').primaryKey(),
|
||||
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||
name: text('name').notNull(),
|
||||
repeat: text('repeat', { enum: ['none', 'daily', 'weekly', 'monthly', 'custom'] }).notNull().default('none'),
|
||||
repeatInterval: integer('repeat_interval').notNull().default(1),
|
||||
repeatDays: text('repeat_days').notNull().default(''),
|
||||
createdAt: bigint('created_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
updatedAt: bigint('updated_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
});
|
||||
export const tombstones = pgTable(
|
||||
'tombstones',
|
||||
{
|
||||
entity: text('entity', { enum: ENTITIES }).notNull(),
|
||||
entityId: text('entity_id').notNull(),
|
||||
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||
updatedAt: bigint('updated_at', { mode: 'number' }).notNull(),
|
||||
},
|
||||
(t) => ({
|
||||
pk: primaryKey({ columns: [t.entity, t.entityId] }),
|
||||
userIdx: index('tombstones_user_updated_idx').on(t.userId, t.updatedAt),
|
||||
})
|
||||
);
|
||||
|
||||
// Define subtasks with explicit type to avoid circular reference
|
||||
export const subtasks = pgTable('subtasks', {
|
||||
id: text('id').primaryKey(),
|
||||
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||
taskId: text('task_id').notNull().references(() => tasks.id, { onDelete: 'cascade' }),
|
||||
parentSubtaskId: text('parent_subtask_id').references(() => subtasks.id, { onDelete: 'cascade' }),
|
||||
parentSubtaskId: text('parent_subtask_id').references((): AnyPgColumn => subtasks.id, { onDelete: 'cascade' }),
|
||||
categoryId: text('category_id').references(() => categories.id, { onDelete: 'set null' }),
|
||||
tags: text('tags').notNull().default(''),
|
||||
title: text('title').notNull(),
|
||||
description: text('description').notNull().default(''),
|
||||
priority: text('priority', { enum: ['none', 'low', 'medium', 'high', 'critical'] }).notNull().default('none'),
|
||||
@@ -94,6 +107,21 @@ export const subtasks = pgTable('subtasks', {
|
||||
order: integer('order').notNull().default(0),
|
||||
createdAt: bigint('created_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
updatedAt: bigint('updated_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
}, (t) => ({
|
||||
parentIdx: index('subtasks_parent_idx').on(t.parentSubtaskId),
|
||||
taskIdx: index('subtasks_task_idx').on(t.taskId),
|
||||
userIdx: index('subtasks_user_idx').on(t.userId),
|
||||
}));
|
||||
|
||||
export const repeatProfiles = pgTable('repeat_profiles', {
|
||||
id: text('id').primaryKey(),
|
||||
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||
name: text('name').notNull(),
|
||||
repeat: text('repeat', { enum: ['none', 'daily', 'weekly', 'monthly', 'custom'] }).notNull().default('none'),
|
||||
repeatInterval: integer('repeat_interval').notNull().default(1),
|
||||
repeatDays: text('repeat_days').notNull().default(''),
|
||||
createdAt: bigint('created_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
updatedAt: bigint('updated_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
});
|
||||
|
||||
export const userSettings = pgTable('user_settings', {
|
||||
|
||||
@@ -9,6 +9,7 @@ import userRoutes from './routes/users';
|
||||
import friendRoutes from './routes/friends';
|
||||
import repeatProfileRoutes from './routes/repeatProfiles';
|
||||
import syncRoutes from './routes/sync';
|
||||
import pushNotificationRoutes from './routes/pushNotifications';
|
||||
import { errorHandler, notFoundHandler } from './middleware/errorHandler';
|
||||
|
||||
const app = express();
|
||||
@@ -34,6 +35,7 @@ app.use('/api/users', userRoutes);
|
||||
app.use('/api/repeat-profiles', repeatProfileRoutes);
|
||||
app.use('/api/sync', syncRoutes);
|
||||
app.use('/api/friends', friendRoutes);
|
||||
app.use('/api/push', pushNotificationRoutes);
|
||||
|
||||
// 404 handler
|
||||
app.use(notFoundHandler);
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { asyncHandler } from '../utils/asyncHandler';
|
||||
import { db } from '../db';
|
||||
import { users } from '../db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { AppError } from '../middleware/errorHandler';
|
||||
import { z } from 'zod';
|
||||
import { sendPushNotification, isExpoPushToken, createTaskReminderPayload } from '../services/pushNotifications';
|
||||
|
||||
const router = Router();
|
||||
|
||||
const registerPushTokenSchema = z.object({
|
||||
pushToken: z.string().min(1),
|
||||
});
|
||||
|
||||
router.post('/push-token', asyncHandler(async (req: Request, res: Response) => {
|
||||
if (!req.user) {
|
||||
throw new AppError('UNAUTHORIZED', 'Authentication required', 401);
|
||||
}
|
||||
|
||||
const data = registerPushTokenSchema.parse(req.body);
|
||||
|
||||
if (!isExpoPushToken(data.pushToken)) {
|
||||
throw new AppError('INVALID_PUSH_TOKEN', 'Invalid Expo push token format', 400);
|
||||
}
|
||||
|
||||
await db
|
||||
.update(users)
|
||||
.set({
|
||||
expoPushToken: data.pushToken,
|
||||
pushTokenUpdatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(users.id, req.user.userId));
|
||||
|
||||
res.json({ success: true });
|
||||
}));
|
||||
|
||||
router.delete('/push-token', asyncHandler(async (req: Request, res: Response) => {
|
||||
if (!req.user) {
|
||||
throw new AppError('UNAUTHORIZED', 'Authentication required', 401);
|
||||
}
|
||||
|
||||
await db
|
||||
.update(users)
|
||||
.set({
|
||||
expoPushToken: null,
|
||||
pushTokenUpdatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(users.id, req.user.userId));
|
||||
|
||||
res.json({ success: true });
|
||||
}));
|
||||
|
||||
router.post('/test', asyncHandler(async (req: Request, res: Response) => {
|
||||
if (!req.user) {
|
||||
throw new AppError('UNAUTHORIZED', 'Authentication required', 401);
|
||||
}
|
||||
|
||||
const user = await db.select().from(users).where(eq(users.id, req.user.userId)).limit(1);
|
||||
if (user.length === 0 || !user[0].expoPushToken) {
|
||||
throw new AppError('NO_PUSH_TOKEN', 'No push token registered', 400);
|
||||
}
|
||||
|
||||
const ticket = await sendPushNotification({
|
||||
to: user[0].expoPushToken!,
|
||||
title: 'Test Notification',
|
||||
body: 'Push notifications are working!',
|
||||
data: { type: 'test' },
|
||||
sound: 'default',
|
||||
});
|
||||
|
||||
res.json({ success: true, ticket });
|
||||
}));
|
||||
|
||||
router.post('/send-reminder', asyncHandler(async (req: Request, res: Response) => {
|
||||
if (!req.user) {
|
||||
throw new AppError('UNAUTHORIZED', 'Authentication required', 401);
|
||||
}
|
||||
|
||||
const { taskId, taskTitle, dueTime, repeat } = req.body;
|
||||
if (!taskId || !taskTitle || !dueTime) {
|
||||
throw new AppError('INVALID_PAYLOAD', 'Missing required fields', 400);
|
||||
}
|
||||
|
||||
const user = await db.select().from(users).where(eq(users.id, req.user.userId)).limit(1);
|
||||
if (user.length === 0 || !user[0].expoPushToken) {
|
||||
throw new AppError('NO_PUSH_TOKEN', 'No push token registered', 400);
|
||||
}
|
||||
|
||||
const ticket = await sendPushNotification(
|
||||
createTaskReminderPayload(user[0].expoPushToken!, taskTitle, dueTime, taskId, repeat)
|
||||
);
|
||||
|
||||
res.json({ success: true, ticket });
|
||||
}));
|
||||
|
||||
export default router;
|
||||
@@ -2,7 +2,7 @@ import { Router, Request, Response } from 'express';
|
||||
import { asyncHandler } from '../utils/asyncHandler';
|
||||
import { db } from '../db';
|
||||
import { subtasks, tasks } from '../db/schema';
|
||||
import { eq, and, asc, desc } from 'drizzle-orm';
|
||||
import { eq, and, asc, desc, isNull } from 'drizzle-orm';
|
||||
import { authMiddleware } from '../utils/auth';
|
||||
import { AppError } from '../middleware/errorHandler';
|
||||
import { subtaskCreateSchema, subtaskUpdateSchema } from '../utils/validation';
|
||||
@@ -70,7 +70,7 @@ router.post('/task/:taskId', asyncHandler(async (req: Request, res: Response) =>
|
||||
.where(and(
|
||||
eq(subtasks.taskId, req.params.taskId),
|
||||
eq(subtasks.userId, userId),
|
||||
parentSubtaskId ? eq(subtasks.parentSubtaskId, parentSubtaskId) : eq(subtasks.parentSubtaskId, null)
|
||||
parentSubtaskId ? eq(subtasks.parentSubtaskId, parentSubtaskId) : isNull(subtasks.parentSubtaskId)
|
||||
))
|
||||
.orderBy(desc(subtasks.order))
|
||||
.limit(1);
|
||||
@@ -82,6 +82,8 @@ router.post('/task/:taskId', asyncHandler(async (req: Request, res: Response) =>
|
||||
userId,
|
||||
taskId: req.params.taskId,
|
||||
parentSubtaskId,
|
||||
categoryId: data.categoryId ?? null,
|
||||
tags: data.tags ?? '',
|
||||
title: data.title,
|
||||
description: data.description ?? '',
|
||||
priority: data.priority ?? 'none',
|
||||
@@ -117,7 +119,7 @@ router.patch('/:id', asyncHandler(async (req: Request, res: Response) => {
|
||||
const data = subtaskUpdateSchema.parse(req.body);
|
||||
const userId = req.user!.userId;
|
||||
|
||||
const existing = await db
|
||||
const existing: any[] = await db
|
||||
.select()
|
||||
.from(subtasks)
|
||||
.where(and(eq(subtasks.id, req.params.id), eq(subtasks.userId, userId)))
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { asyncHandler } from '../utils/asyncHandler';
|
||||
import { db } from '../db';
|
||||
import { categories, tasks, subtasks, repeatProfiles, users, friendships } from '../db/schema';
|
||||
import { eq, and, gte, lte, asc, or, inArray, sql } from 'drizzle-orm';
|
||||
import { categories, tasks, subtasks, repeatProfiles, users, friendships, tombstones, type EntityName } from '../db/schema';
|
||||
import { eq, and, gte, asc, or, inArray, sql } from 'drizzle-orm';
|
||||
import { authMiddleware } from '../utils/auth';
|
||||
import { AppError } from '../middleware/errorHandler';
|
||||
import { syncQuerySchema, pushChangesSchema, canCompleteTask } from '../utils/validation';
|
||||
@@ -17,55 +17,47 @@ router.get('/', asyncHandler(async (req: Request, res: Response) => {
|
||||
|
||||
const sinceDate = since;
|
||||
|
||||
// Fetch categories changed since timestamp
|
||||
const changedCategories = await db
|
||||
.select()
|
||||
.from(categories)
|
||||
.where(and(eq(categories.userId, userId), gte(categories.updatedAt, sinceDate)))
|
||||
.orderBy(asc(categories.updatedAt));
|
||||
// Fetch all changed entities in parallel
|
||||
const [changedCategories, changedTasks, changedSubtasks, changedRepeatProfiles, changedFriendships, changedTombstones] = await Promise.all([
|
||||
db
|
||||
.select()
|
||||
.from(categories)
|
||||
.where(and(eq(categories.userId, userId), gte(categories.updatedAt, sinceDate)))
|
||||
.orderBy(asc(categories.updatedAt)),
|
||||
|
||||
// Fetch tasks changed since timestamp
|
||||
const changedTasks = await db
|
||||
.select()
|
||||
.from(tasks)
|
||||
.where(and(eq(tasks.userId, userId), gte(tasks.updatedAt, sinceDate)))
|
||||
.orderBy(asc(tasks.updatedAt));
|
||||
db
|
||||
.select()
|
||||
.from(tasks)
|
||||
.where(and(eq(tasks.userId, userId), gte(tasks.updatedAt, sinceDate)))
|
||||
.orderBy(asc(tasks.updatedAt)),
|
||||
|
||||
// Fetch subtasks changed since timestamp
|
||||
const taskIds = changedTasks.map(t => t.id);
|
||||
let changedSubtasks: any[] = [];
|
||||
|
||||
if (taskIds.length > 0) {
|
||||
changedSubtasks = await db
|
||||
db
|
||||
.select()
|
||||
.from(subtasks)
|
||||
.where(and(eq(subtasks.userId, userId), gte(subtasks.updatedAt, sinceDate)))
|
||||
.orderBy(asc(subtasks.updatedAt));
|
||||
} else {
|
||||
// Also fetch subtasks for tasks that might have been deleted (we track by updatedAt)
|
||||
changedSubtasks = await db
|
||||
.orderBy(asc(subtasks.updatedAt)),
|
||||
|
||||
db
|
||||
.select()
|
||||
.from(subtasks)
|
||||
.where(and(eq(subtasks.userId, userId), gte(subtasks.updatedAt, sinceDate)))
|
||||
.orderBy(asc(subtasks.updatedAt));
|
||||
}
|
||||
.from(repeatProfiles)
|
||||
.where(and(eq(repeatProfiles.userId, userId), gte(repeatProfiles.updatedAt, sinceDate)))
|
||||
.orderBy(asc(repeatProfiles.updatedAt)),
|
||||
|
||||
// Fetch repeat profiles changed since timestamp
|
||||
const changedRepeatProfiles = await db
|
||||
.select()
|
||||
.from(repeatProfiles)
|
||||
.where(and(eq(repeatProfiles.userId, userId), gte(repeatProfiles.updatedAt, sinceDate)))
|
||||
.orderBy(asc(repeatProfiles.updatedAt));
|
||||
db
|
||||
.select()
|
||||
.from(friendships)
|
||||
.where(and(
|
||||
or(eq(friendships.userId, userId), eq(friendships.friendId, userId)),
|
||||
gte(friendships.updatedAt, sinceDate)
|
||||
))
|
||||
.orderBy(asc(friendships.updatedAt)),
|
||||
|
||||
// Fetch friendships changed since timestamp
|
||||
const changedFriendships = await db
|
||||
.select()
|
||||
.from(friendships)
|
||||
.where(and(
|
||||
or(eq(friendships.userId, userId), eq(friendships.friendId, userId)),
|
||||
gte(friendships.updatedAt, sinceDate)
|
||||
))
|
||||
.orderBy(asc(friendships.updatedAt));
|
||||
db
|
||||
.select()
|
||||
.from(tombstones)
|
||||
.where(and(eq(tombstones.userId, userId), gte(tombstones.updatedAt, sinceDate)))
|
||||
.orderBy(asc(tombstones.updatedAt)),
|
||||
]);
|
||||
|
||||
const timestamp = Date.now();
|
||||
|
||||
@@ -75,6 +67,7 @@ router.get('/', asyncHandler(async (req: Request, res: Response) => {
|
||||
subtasks: changedSubtasks,
|
||||
repeatProfiles: changedRepeatProfiles,
|
||||
friendships: changedFriendships,
|
||||
deleted: changedTombstones.map((t) => ({ entity: t.entity, id: t.entityId, updatedAt: t.updatedAt })),
|
||||
timestamp,
|
||||
});
|
||||
}));
|
||||
@@ -125,6 +118,27 @@ router.post('/push', asyncHandler(async (req: Request, res: Response) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize assignee references: only real user ids may be stored (FK).
|
||||
// Stale/unknown assignee ids are silently dropped to null instead of
|
||||
// failing the whole push transaction.
|
||||
const assigneeIds = new Set<string>();
|
||||
for (const task of data.changes.tasks ?? []) {
|
||||
if (task.assigneeId) assigneeIds.add(task.assigneeId);
|
||||
}
|
||||
for (const sub of data.changes.subtasks ?? []) {
|
||||
if (sub.assigneeId) assigneeIds.add(sub.assigneeId);
|
||||
}
|
||||
const validAssignees = new Set<string>();
|
||||
if (assigneeIds.size > 0) {
|
||||
const rows = await tx
|
||||
.select({ id: users.id })
|
||||
.from(users)
|
||||
.where(inArray(users.id, [...assigneeIds]));
|
||||
for (const r of rows) validAssignees.add(r.id);
|
||||
}
|
||||
const sanitizeAssignee = (a: string | null | undefined): string | null =>
|
||||
a && validAssignees.has(a) ? a : null;
|
||||
|
||||
// Process categories
|
||||
if (data.changes.categories && data.changes.categories.length > 0) {
|
||||
for (const cat of data.changes.categories) {
|
||||
@@ -185,7 +199,7 @@ router.post('/push', asyncHandler(async (req: Request, res: Response) => {
|
||||
resolution: 'server_wins',
|
||||
});
|
||||
continue; // Server wins
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent completing tasks with future due dates
|
||||
if (task.completed === true) {
|
||||
@@ -203,30 +217,33 @@ router.post('/push', asyncHandler(async (req: Request, res: Response) => {
|
||||
}
|
||||
|
||||
await tx
|
||||
.update(tasks)
|
||||
.set({
|
||||
title: task.title,
|
||||
description: task.description,
|
||||
categoryId: task.categoryId,
|
||||
priority: task.priority,
|
||||
completed: task.completed,
|
||||
dueDate: task.dueDate,
|
||||
dueTime: task.dueTime ?? '',
|
||||
endTime: task.endTime ?? '',
|
||||
allDay: task.allDay ?? false,
|
||||
repeat: task.repeat ?? 'none',
|
||||
repeatInterval: task.repeatInterval ?? 1,
|
||||
repeatDays: task.repeatDays ?? '',
|
||||
seriesId: task.seriesId ?? '',
|
||||
reminder: task.reminder ?? 'none',
|
||||
reminders: task.reminders ?? '',
|
||||
assigneeId: task.assigneeId ?? null,
|
||||
updatedAt: task.updatedAt,
|
||||
})
|
||||
.where(and(eq(tasks.id, task.id), eq(tasks.userId, userId)));
|
||||
.update(tasks)
|
||||
.set({
|
||||
title: task.title,
|
||||
description: task.description,
|
||||
categoryId: task.categoryId,
|
||||
tags: task.tags ?? '',
|
||||
priority: task.priority,
|
||||
completed: task.completed,
|
||||
dueDate: task.dueDate,
|
||||
dueTime: task.dueTime ?? '',
|
||||
endTime: task.endTime ?? '',
|
||||
allDay: task.allDay ?? false,
|
||||
repeat: task.repeat ?? 'none',
|
||||
repeatInterval: task.repeatInterval ?? 1,
|
||||
repeatDays: task.repeatDays ?? '',
|
||||
seriesId: task.seriesId ?? '',
|
||||
reminder: task.reminder ?? 'none',
|
||||
reminders: task.reminders ?? '',
|
||||
assigneeId: sanitizeAssignee(task.assigneeId),
|
||||
completedAt: task.completedAt ?? null,
|
||||
updatedAt: task.updatedAt,
|
||||
})
|
||||
.where(and(eq(tasks.id, task.id), eq(tasks.userId, userId)));
|
||||
} else {
|
||||
await tx.insert(tasks).values({
|
||||
...task,
|
||||
assigneeId: sanitizeAssignee(task.assigneeId),
|
||||
allDay: task.allDay ?? false,
|
||||
userId,
|
||||
});
|
||||
@@ -260,6 +277,9 @@ router.post('/push', asyncHandler(async (req: Request, res: Response) => {
|
||||
.update(subtasks)
|
||||
.set({
|
||||
taskId: sub.taskId,
|
||||
parentSubtaskId: sub.parentSubtaskId ?? null,
|
||||
categoryId: sub.categoryId ?? null,
|
||||
tags: sub.tags ?? '',
|
||||
title: sub.title,
|
||||
description: sub.description ?? '',
|
||||
priority: sub.priority ?? 'none',
|
||||
@@ -274,7 +294,7 @@ router.post('/push', asyncHandler(async (req: Request, res: Response) => {
|
||||
seriesId: sub.seriesId ?? '',
|
||||
reminder: sub.reminder ?? 'none',
|
||||
reminders: sub.reminders ?? '',
|
||||
assigneeId: sub.assigneeId ?? null,
|
||||
assigneeId: sanitizeAssignee(sub.assigneeId),
|
||||
order: sub.order,
|
||||
updatedAt: sub.updatedAt,
|
||||
})
|
||||
@@ -282,6 +302,7 @@ router.post('/push', asyncHandler(async (req: Request, res: Response) => {
|
||||
} else {
|
||||
await tx.insert(subtasks).values({
|
||||
...sub,
|
||||
assigneeId: sanitizeAssignee(sub.assigneeId),
|
||||
userId,
|
||||
});
|
||||
}
|
||||
@@ -365,6 +386,14 @@ router.post('/push', asyncHandler(async (req: Request, res: Response) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process deletions (tombstones) - last so they see the state produced
|
||||
// by the upserts above and resolve by last-writer-wins.
|
||||
if (data.deleted && data.deleted.length > 0) {
|
||||
for (const deleted of data.deleted) {
|
||||
await applyTombstone(tx, deleted.entity, deleted.id, deleted.updatedAt, userId, conflicts);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Sync push error:', error);
|
||||
@@ -378,4 +407,197 @@ router.post('/push', asyncHandler(async (req: Request, res: Response) => {
|
||||
});
|
||||
}));
|
||||
|
||||
// Upsert a tombstone row, keeping the latest updatedAt.
|
||||
async function upsertTombstone(
|
||||
tx: any,
|
||||
entity: EntityName,
|
||||
entityId: string,
|
||||
updatedAt: number,
|
||||
userId: string
|
||||
): Promise<void> {
|
||||
const existing = await tx
|
||||
.select({ updatedAt: tombstones.updatedAt })
|
||||
.from(tombstones)
|
||||
.where(and(eq(tombstones.entity, entity), eq(tombstones.entityId, entityId)))
|
||||
.limit(1);
|
||||
|
||||
const merged = Math.max(existing[0]?.updatedAt ?? 0, updatedAt);
|
||||
if (existing.length > 0) {
|
||||
await tx
|
||||
.update(tombstones)
|
||||
.set({ updatedAt: merged })
|
||||
.where(and(eq(tombstones.entity, entity), eq(tombstones.entityId, entityId)));
|
||||
} else {
|
||||
await tx.insert(tombstones).values({ entity, entityId, userId, updatedAt: merged });
|
||||
}
|
||||
}
|
||||
|
||||
// Apply a client deletion. LWW: if the server row is newer than the deletion
|
||||
// timestamp, the deletion is rejected (server_wins conflict) so the client
|
||||
// re-pulls the row. Accepted deletions cascade tombstones to every FK-cascaded
|
||||
// child so all devices remove them too.
|
||||
async function applyTombstone(
|
||||
tx: any,
|
||||
entity: EntityName,
|
||||
id: string,
|
||||
deletedAt: number,
|
||||
userId: string,
|
||||
conflicts: any[]
|
||||
): Promise<void> {
|
||||
const tombstoneOf = (e: EntityName, ids: string[]) => ids.forEach((i) => upsertTombstone(tx, e, i, deletedAt, userId));
|
||||
|
||||
if (entity === 'tasks') {
|
||||
const row = await tx
|
||||
.select()
|
||||
.from(tasks)
|
||||
.where(and(eq(tasks.id, id), eq(tasks.userId, userId)))
|
||||
.limit(1);
|
||||
if (row.length === 0) {
|
||||
await upsertTombstone(tx, entity, id, deletedAt, userId);
|
||||
return;
|
||||
}
|
||||
if (row[0].updatedAt > deletedAt) {
|
||||
conflicts.push({
|
||||
entity: 'tasks',
|
||||
id,
|
||||
serverVersion: row[0],
|
||||
clientVersion: { id, updatedAt: deletedAt },
|
||||
resolution: 'server_wins',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const children = await tx
|
||||
.select({ id: subtasks.id })
|
||||
.from(subtasks)
|
||||
.where(and(eq(subtasks.taskId, id), eq(subtasks.userId, userId)));
|
||||
tombstoneOf('subtasks', children.map((c: any) => c.id));
|
||||
await tx.delete(tasks).where(and(eq(tasks.id, id), eq(tasks.userId, userId)));
|
||||
await upsertTombstone(tx, entity, id, deletedAt, userId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (entity === 'categories') {
|
||||
const row = await tx
|
||||
.select()
|
||||
.from(categories)
|
||||
.where(and(eq(categories.id, id), eq(categories.userId, userId)))
|
||||
.limit(1);
|
||||
if (row.length === 0) {
|
||||
await upsertTombstone(tx, entity, id, deletedAt, userId);
|
||||
return;
|
||||
}
|
||||
if (row[0].updatedAt > deletedAt) {
|
||||
conflicts.push({
|
||||
entity: 'categories',
|
||||
id,
|
||||
serverVersion: row[0],
|
||||
clientVersion: { id, updatedAt: deletedAt },
|
||||
resolution: 'server_wins',
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Deleting the category cascades its tasks (and their subtasks) -
|
||||
// tombstone all of them so every client removes them.
|
||||
const catTasks = await tx
|
||||
.select()
|
||||
.from(tasks)
|
||||
.where(and(eq(tasks.categoryId, id), eq(tasks.userId, userId)));
|
||||
for (const taskRow of catTasks) {
|
||||
const subIds = await tx
|
||||
.select({ id: subtasks.id })
|
||||
.from(subtasks)
|
||||
.where(and(eq(subtasks.taskId, taskRow.id), eq(subtasks.userId, userId)));
|
||||
tombstoneOf('subtasks', subIds.map((s: any) => s.id));
|
||||
tombstoneOf('tasks', [taskRow.id]);
|
||||
}
|
||||
await tx.delete(categories).where(and(eq(categories.id, id), eq(categories.userId, userId)));
|
||||
await upsertTombstone(tx, entity, id, deletedAt, userId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (entity === 'subtasks') {
|
||||
const row = await tx
|
||||
.select()
|
||||
.from(subtasks)
|
||||
.where(and(eq(subtasks.id, id), eq(subtasks.userId, userId)))
|
||||
.limit(1);
|
||||
if (row.length === 0) {
|
||||
await upsertTombstone(tx, entity, id, deletedAt, userId);
|
||||
return;
|
||||
}
|
||||
if (row[0].updatedAt > deletedAt) {
|
||||
conflicts.push({
|
||||
entity: 'subtasks',
|
||||
id,
|
||||
serverVersion: row[0],
|
||||
clientVersion: { id, updatedAt: deletedAt },
|
||||
resolution: 'server_wins',
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Deleting a parent subtask cascades its children in PG - tombstone them.
|
||||
const children = await tx
|
||||
.select({ id: subtasks.id })
|
||||
.from(subtasks)
|
||||
.where(and(eq(subtasks.parentSubtaskId, id), eq(subtasks.userId, userId)));
|
||||
tombstoneOf('subtasks', children.map((c: any) => c.id));
|
||||
await tx.delete(subtasks).where(and(eq(subtasks.id, id), eq(subtasks.userId, userId)));
|
||||
await upsertTombstone(tx, entity, id, deletedAt, userId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (entity === 'repeatProfiles') {
|
||||
const row = await tx
|
||||
.select()
|
||||
.from(repeatProfiles)
|
||||
.where(and(eq(repeatProfiles.id, id), eq(repeatProfiles.userId, userId)))
|
||||
.limit(1);
|
||||
if (row.length === 0) {
|
||||
await upsertTombstone(tx, entity, id, deletedAt, userId);
|
||||
return;
|
||||
}
|
||||
if (row[0].updatedAt > deletedAt) {
|
||||
conflicts.push({
|
||||
entity: 'repeatProfiles',
|
||||
id,
|
||||
serverVersion: row[0],
|
||||
clientVersion: { id, updatedAt: deletedAt },
|
||||
resolution: 'server_wins',
|
||||
});
|
||||
return;
|
||||
}
|
||||
await tx.delete(repeatProfiles).where(and(eq(repeatProfiles.id, id), eq(repeatProfiles.userId, userId)));
|
||||
await upsertTombstone(tx, entity, id, deletedAt, userId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (entity === 'friendships') {
|
||||
const row = await tx
|
||||
.select()
|
||||
.from(friendships)
|
||||
.where(and(
|
||||
eq(friendships.id, id),
|
||||
or(eq(friendships.userId, userId), eq(friendships.friendId, userId))
|
||||
))
|
||||
.limit(1);
|
||||
if (row.length === 0) {
|
||||
await upsertTombstone(tx, entity, id, deletedAt, userId);
|
||||
return;
|
||||
}
|
||||
if (row[0].updatedAt > deletedAt) {
|
||||
conflicts.push({
|
||||
entity: 'friendships',
|
||||
id,
|
||||
serverVersion: row[0],
|
||||
clientVersion: { id, updatedAt: deletedAt },
|
||||
resolution: 'server_wins',
|
||||
});
|
||||
return;
|
||||
}
|
||||
await tx.delete(friendships).where(eq(friendships.id, id));
|
||||
await upsertTombstone(tx, entity, id, deletedAt, userId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export default router;
|
||||
@@ -116,15 +116,17 @@ router.post('/', asyncHandler(async (req: Request, res: Response) => {
|
||||
const userId = req.user!.userId;
|
||||
const now = Date.now();
|
||||
|
||||
// Verify category exists and belongs to user
|
||||
const cat = await db
|
||||
.select()
|
||||
.from(categories)
|
||||
.where(and(eq(categories.id, data.categoryId), eq(categories.userId, userId)))
|
||||
.limit(1);
|
||||
// Verify category exists and belongs to user (optional - tasks may be uncategorized)
|
||||
if (data.categoryId) {
|
||||
const cat = await db
|
||||
.select()
|
||||
.from(categories)
|
||||
.where(and(eq(categories.id, data.categoryId), eq(categories.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (cat.length === 0) {
|
||||
throw new AppError('NOT_FOUND', 'Category not found', 404);
|
||||
if (cat.length === 0) {
|
||||
throw new AppError('NOT_FOUND', 'Category not found', 404);
|
||||
}
|
||||
}
|
||||
|
||||
const taskId = `task_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
||||
@@ -133,6 +135,7 @@ router.post('/', asyncHandler(async (req: Request, res: Response) => {
|
||||
id: taskId,
|
||||
userId,
|
||||
categoryId: data.categoryId,
|
||||
tags: data.tags ?? '',
|
||||
title: data.title,
|
||||
description: data.description ?? '',
|
||||
priority: data.priority ?? 'none',
|
||||
@@ -157,6 +160,8 @@ router.post('/', asyncHandler(async (req: Request, res: Response) => {
|
||||
userId,
|
||||
taskId,
|
||||
title: st.title,
|
||||
categoryId: (st as any).categoryId ?? null,
|
||||
tags: (st as any).tags ?? '',
|
||||
completed: false,
|
||||
order: index,
|
||||
createdAt: now,
|
||||
@@ -261,12 +266,14 @@ router.post('/batch', asyncHandler(async (req: Request, res: Response) => {
|
||||
for (const op of operations) {
|
||||
try {
|
||||
if (op.type === 'create') {
|
||||
const cat = await db
|
||||
.select()
|
||||
.from(categories)
|
||||
.where(and(eq(categories.id, op.data.categoryId), eq(categories.userId, userId)))
|
||||
.limit(1);
|
||||
if (cat.length === 0) throw new AppError('NOT_FOUND', 'Category not found', 404);
|
||||
if (op.data.categoryId) {
|
||||
const cat = await db
|
||||
.select()
|
||||
.from(categories)
|
||||
.where(and(eq(categories.id, op.data.categoryId), eq(categories.userId, userId)))
|
||||
.limit(1);
|
||||
if (cat.length === 0) throw new AppError('NOT_FOUND', 'Category not found', 404);
|
||||
}
|
||||
|
||||
const taskId = `task_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
||||
const now = Date.now();
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
const EXPO_PUSH_URL = 'https://exp.host/--/api/v2/push/send';
|
||||
|
||||
export interface PushNotificationPayload {
|
||||
to: string;
|
||||
title: string;
|
||||
body: string;
|
||||
data?: Record<string, any>;
|
||||
sound?: string;
|
||||
badge?: number;
|
||||
priority?: 'default' | 'normal' | 'high';
|
||||
channelId?: string;
|
||||
}
|
||||
|
||||
export interface PushTicket {
|
||||
id: string;
|
||||
status: 'ok' | 'error';
|
||||
message?: string;
|
||||
details?: {
|
||||
error?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PushReceipt {
|
||||
status: 'ok' | 'error';
|
||||
message?: string;
|
||||
details?: {
|
||||
error?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ExpoPushResponse {
|
||||
data?: PushTicket[] | Record<string, PushReceipt>;
|
||||
}
|
||||
|
||||
export async function sendPushNotification(payload: PushNotificationPayload): Promise<PushTicket> {
|
||||
const response = await fetch(EXPO_PUSH_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const result = (await response.json()) as ExpoPushResponse;
|
||||
if (result.data && Array.isArray(result.data)) {
|
||||
return result.data[0] ?? { id: '', status: 'error', message: 'No ticket returned' };
|
||||
}
|
||||
return { id: '', status: 'error', message: 'No ticket returned' };
|
||||
}
|
||||
|
||||
export async function sendPushNotifications(payloads: PushNotificationPayload[]): Promise<PushTicket[]> {
|
||||
const chunks: PushNotificationPayload[][] = [];
|
||||
for (let i = 0; i < payloads.length; i += 100) {
|
||||
chunks.push(payloads.slice(i, i + 100));
|
||||
}
|
||||
|
||||
const tickets: PushTicket[] = [];
|
||||
for (const chunk of chunks) {
|
||||
const response = await fetch(EXPO_PUSH_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify(chunk),
|
||||
});
|
||||
const result = (await response.json()) as ExpoPushResponse;
|
||||
if (result.data && Array.isArray(result.data)) {
|
||||
tickets.push(...result.data);
|
||||
}
|
||||
}
|
||||
return tickets;
|
||||
}
|
||||
|
||||
export async function getPushReceipts(ticketIds: string[]): Promise<PushReceipt[]> {
|
||||
if (ticketIds.length === 0) return [];
|
||||
|
||||
const response = await fetch(`${EXPO_PUSH_URL}/receipts`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ ids: ticketIds }),
|
||||
});
|
||||
|
||||
const result = (await response.json()) as ExpoPushResponse;
|
||||
const receipts: PushReceipt[] = [];
|
||||
if (result.data && !Array.isArray(result.data)) {
|
||||
for (const id of ticketIds) {
|
||||
receipts.push(result.data[id] ?? { status: 'error', message: 'No receipt found' });
|
||||
}
|
||||
}
|
||||
return receipts;
|
||||
}
|
||||
|
||||
export function isExpoPushToken(token: string): boolean {
|
||||
return token.startsWith('ExponentPushToken[') || token.startsWith('ExpoPushToken[');
|
||||
}
|
||||
|
||||
export function createTaskReminderPayload(
|
||||
pushToken: string,
|
||||
taskTitle: string,
|
||||
dueTime: string,
|
||||
taskId: string,
|
||||
repeat?: string
|
||||
): PushNotificationPayload {
|
||||
return {
|
||||
to: pushToken,
|
||||
title: taskTitle,
|
||||
body: `Due ${dueTime}${repeat && repeat !== 'none' ? ' · repeating' : ''}`,
|
||||
data: { type: 'task_reminder', taskId },
|
||||
sound: 'default',
|
||||
priority: 'high',
|
||||
channelId: 'task-reminders',
|
||||
};
|
||||
}
|
||||
|
||||
export function createFriendRequestPayload(
|
||||
pushToken: string,
|
||||
fromUsername: string,
|
||||
friendshipId: string
|
||||
): PushNotificationPayload {
|
||||
return {
|
||||
to: pushToken,
|
||||
title: 'New Friend Request',
|
||||
body: `${fromUsername} wants to be your friend`,
|
||||
data: { type: 'friend_request', friendshipId },
|
||||
sound: 'default',
|
||||
priority: 'high',
|
||||
channelId: 'friend-requests',
|
||||
};
|
||||
}
|
||||
|
||||
export function createTaskAssignmentPayload(
|
||||
pushToken: string,
|
||||
taskTitle: string,
|
||||
fromUsername: string,
|
||||
taskId: string
|
||||
): PushNotificationPayload {
|
||||
return {
|
||||
to: pushToken,
|
||||
title: 'Task Assigned',
|
||||
body: `${fromUsername} assigned you "${taskTitle}"`,
|
||||
data: { type: 'task_assignment', taskId },
|
||||
sound: 'default',
|
||||
priority: 'high',
|
||||
channelId: 'task-assignments',
|
||||
};
|
||||
}
|
||||
@@ -21,29 +21,46 @@ export const categoryUpdateSchema = z.object({
|
||||
|
||||
export const repeatSchema = z.enum(['none', 'daily', 'weekly', 'monthly', 'custom']);
|
||||
|
||||
// The client stores '' / 0 for unset repeat values; normalize them so stale or
|
||||
// legacy rows never fail validation on push.
|
||||
export const repeatFieldSchema = repeatSchema
|
||||
.or(z.literal(''))
|
||||
.transform((v) => (v === '' ? 'none' : v))
|
||||
.optional();
|
||||
export const repeatIntervalFieldSchema = z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(30)
|
||||
.transform((v) => Math.max(1, v))
|
||||
.optional();
|
||||
|
||||
export const taskCreateSchema = z.object({
|
||||
title: z.string().min(1).max(100),
|
||||
description: z.string().max(1000).optional(),
|
||||
categoryId: z.string().min(1),
|
||||
categoryId: z.string().max(100).transform((v) => (v === '' ? null : v)).nullable(),
|
||||
tags: z.string().max(500).optional(),
|
||||
priority: z.enum(['none', 'low', 'medium', 'high', 'critical']).optional(),
|
||||
dueDate: z.number().int().min(0).optional(),
|
||||
dueTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().or(z.literal('')).transform(v => v ?? ''),
|
||||
endTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().or(z.literal('')),
|
||||
allDay: z.boolean().optional(),
|
||||
repeat: repeatSchema.optional(),
|
||||
repeatInterval: z.number().int().min(1).max(30).optional(),
|
||||
repeat: repeatFieldSchema,
|
||||
repeatInterval: repeatIntervalFieldSchema,
|
||||
repeatDays: z.string().max(20).optional(),
|
||||
seriesId: z.string().max(50).optional(),
|
||||
reminder: z.enum(['none', 'at_time', '15', '30', '60', '120', '1440']).optional(),
|
||||
reminders: z.string().max(100).optional(),
|
||||
assigneeId: z.string().nullable().optional(),
|
||||
subtasks: z.array(z.object({ title: z.string().min(1).max(100) })).optional(),
|
||||
completedAt: z.number().int().min(0).nullable().optional(),
|
||||
subtasks: z.array(z.object({ title: z.string().min(1).max(100), categoryId: z.string().max(100).nullable().optional().transform((v) => (v === '' ? null : v)) })).optional(),
|
||||
});
|
||||
|
||||
export const taskUpdateSchema = z.object({
|
||||
title: z.string().min(1).max(100).optional(),
|
||||
description: z.string().max(1000).optional(),
|
||||
categoryId: z.string().min(1).optional(),
|
||||
categoryId: z.string().max(100).nullable().optional().transform((v) => (v === '' ? null : v)),
|
||||
tags: z.string().max(500).optional(),
|
||||
priority: z.enum(['none', 'low', 'medium', 'high', 'critical']).optional(),
|
||||
completed: z.boolean().optional(),
|
||||
dueDate: z.number().int().min(0).optional(),
|
||||
@@ -51,18 +68,21 @@ export const taskUpdateSchema = z.object({
|
||||
endTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional(),
|
||||
reminders: z.string().max(100).optional(),
|
||||
assigneeId: z.string().nullable().optional(),
|
||||
completedAt: z.number().int().min(0).nullable().optional(),
|
||||
});
|
||||
|
||||
export const subtaskCreateSchema = z.object({
|
||||
title: z.string().min(1).max(100),
|
||||
description: z.string().max(1000).optional(),
|
||||
categoryId: z.string().max(100).nullable().optional().transform((v) => (v === '' ? null : v)),
|
||||
tags: z.string().max(500).optional(),
|
||||
priority: z.enum(['none', 'low', 'medium', 'high', 'critical']).optional(),
|
||||
dueDate: z.number().int().min(0).optional(),
|
||||
dueTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().or(z.literal('')).transform(v => v ?? ''),
|
||||
endTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().or(z.literal('')),
|
||||
allDay: z.boolean().optional(),
|
||||
repeat: repeatSchema.optional(),
|
||||
repeatInterval: z.number().int().min(1).max(30).optional(),
|
||||
repeat: repeatFieldSchema,
|
||||
repeatInterval: repeatIntervalFieldSchema,
|
||||
repeatDays: z.string().max(20).optional(),
|
||||
seriesId: z.string().max(50).optional(),
|
||||
reminder: z.enum(['none', 'at_time', '15', '30', '60', '120', '1440']).optional(),
|
||||
@@ -75,14 +95,16 @@ export const subtaskCreateSchema = z.object({
|
||||
export const subtaskUpdateSchema = z.object({
|
||||
title: z.string().min(1).max(100).optional(),
|
||||
description: z.string().max(1000).optional(),
|
||||
categoryId: z.string().max(100).nullable().optional().transform((v) => (v === '' ? null : v)),
|
||||
tags: z.string().max(500).optional(),
|
||||
priority: z.enum(['none', 'low', 'medium', 'high', 'critical']).optional(),
|
||||
completed: z.boolean().optional(),
|
||||
dueDate: z.number().int().min(0).optional(),
|
||||
dueTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().or(z.literal('')).transform(v => v === '' ? undefined : v),
|
||||
endTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional(),
|
||||
allDay: z.boolean().optional(),
|
||||
repeat: repeatSchema.optional(),
|
||||
repeatInterval: z.number().int().min(1).max(30).optional(),
|
||||
repeat: repeatFieldSchema,
|
||||
repeatInterval: repeatIntervalFieldSchema,
|
||||
repeatDays: z.string().max(20).optional(),
|
||||
seriesId: z.string().max(50).optional(),
|
||||
reminder: z.enum(['none', 'at_time', '15', '30', '60', '120', '1440']).optional(),
|
||||
@@ -132,14 +154,21 @@ export const friendshipSchema = z.object({
|
||||
updatedAt: z.number(),
|
||||
});
|
||||
|
||||
export const tombstoneSchema = z.object({
|
||||
entity: z.enum(['categories', 'tasks', 'subtasks', 'repeatProfiles', 'friendships']),
|
||||
id: z.string(),
|
||||
updatedAt: z.number().int().min(0),
|
||||
});
|
||||
|
||||
export const pushChangesSchema = z.object({
|
||||
changes: z.object({
|
||||
categories: z.array(categoryCreateSchema.extend({ id: z.string(), createdAt: z.number(), updatedAt: z.number() })).optional(),
|
||||
tasks: z.array(taskCreateSchema.extend({ id: z.string(), completed: z.boolean(), createdAt: z.number(), updatedAt: z.number() })).optional(),
|
||||
tasks: z.array(taskCreateSchema.extend({ id: z.string(), completed: z.boolean(), createdAt: z.number(), updatedAt: z.number(), completedAt: z.number().int().min(0).nullable().optional() })).optional(),
|
||||
subtasks: z.array(subtaskCreateSchema.extend({ id: z.string(), taskId: z.string(), completed: z.boolean(), createdAt: z.number(), updatedAt: z.number() })).optional(),
|
||||
repeatProfiles: z.array(repeatProfileSchema.extend({ id: z.string(), createdAt: z.number(), updatedAt: z.number() })).optional(),
|
||||
friendships: z.array(friendshipSchema).optional(),
|
||||
}),
|
||||
deleted: z.array(tombstoneSchema).optional(),
|
||||
lastPulledAt: z.number().int().min(0),
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
node_modules
|
||||
dist
|
||||
.expo
|
||||
.git
|
||||
*.log
|
||||
.DS_Store
|
||||
android
|
||||
ios
|
||||
coverage
|
||||
*.local
|
||||
@@ -7,6 +7,9 @@ node_modules/
|
||||
.expo/
|
||||
dist/
|
||||
web-build/
|
||||
|
||||
# Electron
|
||||
desktop-build/
|
||||
expo-env.d.ts
|
||||
|
||||
# Native
|
||||
@@ -38,4 +41,3 @@ yarn-error.*
|
||||
|
||||
# generated native folders
|
||||
/ios
|
||||
/android
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
RUN npx expo export -p web
|
||||
|
||||
FROM nginx:alpine AS runner
|
||||
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 8081
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -0,0 +1,19 @@
|
||||
# OSX
|
||||
#
|
||||
.DS_Store
|
||||
|
||||
# Android/IntelliJ
|
||||
#
|
||||
build/
|
||||
.idea
|
||||
.gradle
|
||||
local.properties
|
||||
*.iml
|
||||
*.hprof
|
||||
.cxx/
|
||||
|
||||
# generated inline modules
|
||||
app/src/main/java/inline/
|
||||
|
||||
# Bundle artifacts
|
||||
*.jsbundle
|
||||
@@ -0,0 +1,182 @@
|
||||
apply plugin: "com.android.application"
|
||||
apply plugin: "org.jetbrains.kotlin.android"
|
||||
apply plugin: "com.facebook.react"
|
||||
|
||||
def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
|
||||
|
||||
/**
|
||||
* This is the configuration block to customize your React Native Android app.
|
||||
* By default you don't need to apply any configuration, just uncomment the lines you need.
|
||||
*/
|
||||
react {
|
||||
entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
|
||||
reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
|
||||
hermesCommand = new File(["node", "--print", "require.resolve('hermes-compiler/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/hermesc/%OS-BIN%/hermesc"
|
||||
codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
|
||||
|
||||
enableBundleCompression = (findProperty('android.enableBundleCompression') ?: false).toBoolean()
|
||||
// Use Expo CLI to bundle the app, this ensures the Metro config
|
||||
// works correctly with Expo projects.
|
||||
cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim())
|
||||
bundleCommand = "export:embed"
|
||||
|
||||
/* Folders */
|
||||
// The root of your project, i.e. where "package.json" lives. Default is '../..'
|
||||
// root = file("../../")
|
||||
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
|
||||
// reactNativeDir = file("../../node_modules/react-native")
|
||||
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
|
||||
// codegenDir = file("../../node_modules/@react-native/codegen")
|
||||
|
||||
/* Variants */
|
||||
// The list of variants to that are debuggable. For those we're going to
|
||||
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
|
||||
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
|
||||
// debuggableVariants = ["liteDebug", "prodDebug"]
|
||||
|
||||
/* Bundling */
|
||||
// A list containing the node command and its flags. Default is just 'node'.
|
||||
// nodeExecutableAndArgs = ["node"]
|
||||
|
||||
//
|
||||
// The path to the CLI configuration file. Default is empty.
|
||||
// bundleConfig = file(../rn-cli.config.js)
|
||||
//
|
||||
// The name of the generated asset file containing your JS bundle
|
||||
// bundleAssetName = "MyApplication.android.bundle"
|
||||
//
|
||||
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
|
||||
// entryFile = file("../js/MyApplication.android.js")
|
||||
//
|
||||
// A list of extra flags to pass to the 'bundle' commands.
|
||||
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
|
||||
// extraPackagerArgs = []
|
||||
|
||||
/* Hermes Commands */
|
||||
// The hermes compiler command to run. By default it is 'hermesc'
|
||||
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
|
||||
//
|
||||
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
|
||||
// hermesFlags = ["-O", "-output-source-map"]
|
||||
|
||||
/* Autolinking */
|
||||
autolinkLibrariesWithApp()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this to true in release builds to optimize the app using [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization).
|
||||
*/
|
||||
def enableMinifyInReleaseBuilds = (findProperty('android.enableMinifyInReleaseBuilds') ?: false).toBoolean()
|
||||
|
||||
/**
|
||||
* The preferred build flavor of JavaScriptCore (JSC)
|
||||
*
|
||||
* For example, to use the international variant, you can use:
|
||||
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
|
||||
*
|
||||
* The international variant includes ICU i18n library and necessary data
|
||||
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
||||
* give correct results when using with locales other than en-US. Note that
|
||||
* this variant is about 6MiB larger per architecture than default.
|
||||
*/
|
||||
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
|
||||
|
||||
android {
|
||||
ndkVersion = rootProject.ext.ndkVersion
|
||||
|
||||
buildToolsVersion = rootProject.ext.buildToolsVersion
|
||||
compileSdk = rootProject.ext.compileSdkVersion
|
||||
|
||||
namespace = 'com.anonymous.carryyourlive'
|
||||
defaultConfig {
|
||||
applicationId = 'com.anonymous.carryyourlive'
|
||||
minSdkVersion = rootProject.ext.minSdkVersion
|
||||
targetSdkVersion = rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0.0"
|
||||
|
||||
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
storeFile file('debug.keystore')
|
||||
storePassword 'android'
|
||||
keyAlias 'androiddebugkey'
|
||||
keyPassword 'android'
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
signingConfig = signingConfigs.debug
|
||||
}
|
||||
release {
|
||||
// Caution! In production, you need to generate your own keystore file.
|
||||
// see https://reactnative.dev/docs/signed-apk-android.
|
||||
signingConfig = signingConfigs.debug
|
||||
def enableShrinkResources = findProperty('android.enableShrinkResourcesInReleaseBuilds') ?: 'false'
|
||||
shrinkResources = enableShrinkResources.toBoolean()
|
||||
minifyEnabled enableMinifyInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
def enablePngCrunchInRelease = findProperty('android.enablePngCrunchInReleaseBuilds') ?: 'true'
|
||||
crunchPngs = enablePngCrunchInRelease.toBoolean()
|
||||
}
|
||||
}
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
def enableLegacyPackaging = findProperty('expo.useLegacyPackaging') ?: 'false'
|
||||
useLegacyPackaging = enableLegacyPackaging.toBoolean()
|
||||
}
|
||||
}
|
||||
androidResources {
|
||||
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
}
|
||||
}
|
||||
|
||||
// Apply static values from `gradle.properties` to the `android.packagingOptions`
|
||||
// Accepts values in comma delimited lists, example:
|
||||
// android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini
|
||||
["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop ->
|
||||
// Split option: 'foo,bar' -> ['foo', 'bar']
|
||||
def options = (findProperty("android.packagingOptions.$prop") ?: "").split(",");
|
||||
// Trim all elements in place.
|
||||
for (i in 0..<options.size()) options[i] = options[i].trim();
|
||||
// `[] - ""` is essentially `[""].filter(Boolean)` removing all empty strings.
|
||||
options -= ""
|
||||
|
||||
if (options.length > 0) {
|
||||
println "android.packagingOptions.$prop += $options ($options.length)"
|
||||
// Ex: android.packagingOptions.pickFirsts += '**/SCCS/**'
|
||||
options.each {
|
||||
android.packagingOptions[prop] += it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
|
||||
def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
|
||||
def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
|
||||
def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
|
||||
|
||||
if (isGifEnabled) {
|
||||
// For animated gif support
|
||||
implementation("com.facebook.fresco:animated-gif:${expoLibs.versions.fresco.get()}")
|
||||
}
|
||||
|
||||
if (isWebpEnabled) {
|
||||
// For webp support
|
||||
implementation("com.facebook.fresco:webpsupport:${expoLibs.versions.fresco.get()}")
|
||||
if (isWebpAnimatedEnabled) {
|
||||
// Animated webp support
|
||||
implementation("com.facebook.fresco:animated-webp:${expoLibs.versions.fresco.get()}")
|
||||
}
|
||||
}
|
||||
|
||||
if (hermesEnabled.toBoolean()) {
|
||||
implementation("com.facebook.react:hermes-android")
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# react-native-reanimated
|
||||
-keep class com.swmansion.reanimated.** { *; }
|
||||
-keep class com.facebook.react.turbomodule.** { *; }
|
||||
|
||||
# Add any project specific keep options here:
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
|
||||
</manifest>
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
|
||||
</manifest>
|
||||
@@ -0,0 +1,36 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:replace="android:maxSdkVersion"/>
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:replace="android:maxSdkVersion"/>
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="https"/>
|
||||
</intent>
|
||||
</queries>
|
||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:enableOnBackInvokedCallback="false" android:usesCleartextTraffic="true">
|
||||
<meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/notification_icon_color"/>
|
||||
<meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/notification_icon"/>
|
||||
<meta-data android:name="expo.modules.notifications.default_notification_color" android:resource="@color/notification_icon_color"/>
|
||||
<meta-data android:name="expo.modules.notifications.default_notification_icon" android:resource="@drawable/notification_icon"/>
|
||||
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
|
||||
<meta-data android:name="expo.modules.updates.ENABLE_BSDIFF_PATCH_SUPPORT" android:value="true"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
|
||||
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode|smallestScreenSize|assetsPaths" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="exp+carry-your-live"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.anonymous.carryyourlive
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
|
||||
import com.facebook.react.ReactActivity
|
||||
import com.facebook.react.ReactActivityDelegate
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||
|
||||
import expo.modules.ReactActivityDelegateWrapper
|
||||
|
||||
class MainActivity : ReactActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Set the theme to AppTheme BEFORE onCreate to support
|
||||
// coloring the background, status bar, and navigation bar.
|
||||
// This is required for expo-splash-screen.
|
||||
setTheme(R.style.AppTheme);
|
||||
super.onCreate(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||
* rendering of the component.
|
||||
*/
|
||||
override fun getMainComponentName(): String = "main"
|
||||
|
||||
/**
|
||||
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
|
||||
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
|
||||
*/
|
||||
override fun createReactActivityDelegate(): ReactActivityDelegate {
|
||||
return ReactActivityDelegateWrapper(
|
||||
this,
|
||||
BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
|
||||
object : DefaultReactActivityDelegate(
|
||||
this,
|
||||
mainComponentName,
|
||||
fabricEnabled
|
||||
){})
|
||||
}
|
||||
|
||||
/**
|
||||
* Align the back button behavior with Android S
|
||||
* where moving root activities to background instead of finishing activities.
|
||||
* @see <a href="https://developer.android.com/reference/android/app/Activity#onBackPressed()">onBackPressed</a>
|
||||
*/
|
||||
override fun invokeDefaultOnBackPressed() {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||
if (!moveTaskToBack(false)) {
|
||||
// For non-root activities, use the default implementation to finish them.
|
||||
super.invokeDefaultOnBackPressed()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Use the default back button implementation on Android S
|
||||
// because it's doing more than [Activity.moveTaskToBack] in fact.
|
||||
super.invokeDefaultOnBackPressed()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.anonymous.carryyourlive
|
||||
|
||||
import android.app.Application
|
||||
import android.content.res.Configuration
|
||||
|
||||
import com.facebook.react.PackageList
|
||||
import com.facebook.react.ReactApplication
|
||||
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
|
||||
import com.facebook.react.ReactPackage
|
||||
import com.facebook.react.ReactHost
|
||||
import com.facebook.react.common.ReleaseLevel
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
|
||||
|
||||
import expo.modules.ApplicationLifecycleDispatcher
|
||||
import expo.modules.ExpoReactHostFactory
|
||||
|
||||
class MainApplication : Application(), ReactApplication {
|
||||
|
||||
override val reactHost: ReactHost by lazy {
|
||||
ExpoReactHostFactory.getDefaultReactHost(
|
||||
context = applicationContext,
|
||||
packageList =
|
||||
PackageList(this).packages.apply {
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// add(MyReactNativePackage())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
DefaultNewArchitectureEntryPoint.releaseLevel = try {
|
||||
ReleaseLevel.valueOf(BuildConfig.REACT_NATIVE_RELEASE_LEVEL.uppercase())
|
||||
} catch (e: IllegalArgumentException) {
|
||||
ReleaseLevel.STABLE
|
||||
}
|
||||
loadReactNative(this)
|
||||
ApplicationLifecycleDispatcher.onApplicationCreate(this)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1010 B |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 584 B |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 65 KiB |
@@ -0,0 +1,6 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@color/splashscreen_background"/>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splashscreen_logo"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2014 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
|
||||
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
|
||||
android:insetTop="@dimen/abc_edit_text_inset_top_material"
|
||||
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material"
|
||||
>
|
||||
|
||||
<selector>
|
||||
<!--
|
||||
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
|
||||
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
|
||||
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
|
||||
|
||||
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||
|
||||
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
|
||||
-->
|
||||
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
|
||||
</selector>
|
||||
|
||||
</inset>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 18 KiB |