fixxed color selector in the category settings and fixxed drag and drop
Build APK / build (push) Canceled after 2m0s

subtask
This commit is contained in:
2026-08-09 21:42:54 +02:00
parent 4b6e87c979
commit 4c3d1a118c
99 changed files with 3335 additions and 920 deletions
+513 -136
View File
@@ -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
View File
@@ -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"}
+46 -12
View File
@@ -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
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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"}
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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"}
+101 -9
View File
@@ -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
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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"}
+11
View File
@@ -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;
+1 -1
View File
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
declare const router: import("express-serve-static-core").Router;
export default router;
//# sourceMappingURL=repeatProfiles.d.ts.map
+1
View File
@@ -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"}
+74
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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"}
+28 -2
View File
@@ -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,
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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"}
+281 -5
View File
@@ -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
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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"}
+48 -15
View File
@@ -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() })
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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
View File
@@ -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"}
+14 -1
View File
@@ -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
View File
@@ -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"}
+370 -90
View File
@@ -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
View File
@@ -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"}
+77 -8
View File
@@ -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({
File diff suppressed because one or more lines are too long
+37 -14
View File
@@ -1,6 +1,9 @@
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(),
@@ -40,7 +43,7 @@ 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' }),
title: text('title').notNull(),
description: text('description').notNull().default(''),
priority: text('priority', { enum: ['none', 'low', 'medium', 'high', 'critical'] }).notNull().default('none'),
@@ -56,26 +59,31 @@ 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' }),
title: text('title').notNull(),
description: text('description').notNull().default(''),
priority: text('priority', { enum: ['none', 'low', 'medium', 'high', 'critical'] }).notNull().default('none'),
@@ -94,6 +102,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', {
@@ -105,4 +128,4 @@ export const userSettings = pgTable('user_settings', {
sortBy: text('sort_by', { enum: ['dueDate', 'priority', 'title', 'createdAt'] }).notNull().default('dueDate'),
sortOrder: text('sort_order', { enum: ['asc', 'desc'] }).notNull().default('asc'),
updatedAt: bigint('updated_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
});
});
+3 -3
View File
@@ -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);
@@ -117,7 +117,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)))
+261 -29
View File
@@ -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';
@@ -32,17 +32,15 @@ router.get('/', asyncHandler(async (req: Request, res: Response) => {
.orderBy(asc(tasks.updatedAt));
// Fetch subtasks changed since timestamp
const taskIds = changedTasks.map(t => t.id);
let changedSubtasks: any[] = [];
if (taskIds.length > 0) {
if (changedTasks.length > 0) {
changedSubtasks = await 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)
// Also fetch subtasks for tasks that might have been deleted
changedSubtasks = await db
.select()
.from(subtasks)
@@ -67,6 +65,13 @@ router.get('/', asyncHandler(async (req: Request, res: Response) => {
))
.orderBy(asc(friendships.updatedAt));
// Fetch tombstones (deletions) changed since timestamp
const changedTombstones = await db
.select()
.from(tombstones)
.where(and(eq(tombstones.userId, userId), gte(tombstones.updatedAt, sinceDate)))
.orderBy(asc(tombstones.updatedAt));
const timestamp = Date.now();
res.json({
@@ -75,6 +80,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 +131,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 +212,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 +230,32 @@ 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,
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 +289,7 @@ router.post('/push', asyncHandler(async (req: Request, res: Response) => {
.update(subtasks)
.set({
taskId: sub.taskId,
parentSubtaskId: sub.parentSubtaskId ?? null,
title: sub.title,
description: sub.description ?? '',
priority: sub.priority ?? 'none',
@@ -274,7 +304,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 +312,7 @@ router.post('/push', asyncHandler(async (req: Request, res: Response) => {
} else {
await tx.insert(subtasks).values({
...sub,
assigneeId: sanitizeAssignee(sub.assigneeId),
userId,
});
}
@@ -365,6 +396,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 +417,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;
+18 -14
View File
@@ -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)}`;
@@ -261,12 +263,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();
+32 -9
View File
@@ -21,29 +21,44 @@ 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(),
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(),
completedAt: z.number().int().min(0).nullable().optional(),
subtasks: z.array(z.object({ title: z.string().min(1).max(100) })).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)),
priority: z.enum(['none', 'low', 'medium', 'high', 'critical']).optional(),
completed: z.boolean().optional(),
dueDate: z.number().int().min(0).optional(),
@@ -51,6 +66,7 @@ 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({
@@ -61,8 +77,8 @@ export const subtaskCreateSchema = z.object({
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(),
@@ -81,8 +97,8 @@ export const subtaskUpdateSchema = z.object({
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 +148,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),
});