diff --git a/drizzle/0013_habit_calendar_revision.sql b/drizzle/0013_habit_calendar_revision.sql new file mode 100644 index 0000000..9139f88 --- /dev/null +++ b/drizzle/0013_habit_calendar_revision.sql @@ -0,0 +1 @@ +ALTER TABLE habits ADD COLUMN calendar_revision integer NOT NULL DEFAULT 0; diff --git a/drizzle/meta/0013_snapshot.json b/drizzle/meta/0013_snapshot.json new file mode 100644 index 0000000..b90571e --- /dev/null +++ b/drizzle/meta/0013_snapshot.json @@ -0,0 +1,1225 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "4c5b56f0-04cd-405a-afdd-8e4ca3793332", + "prevId": "39a7a2c2-c411-4e0a-ba41-9e4abdc8fbcf", + "tables": { + "combined_charts": { + "name": "combined_charts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "habit_ids": { + "name": "habit_ids", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "settings": { + "name": "settings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "combined_charts_user_idx": { + "name": "combined_charts_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "combined_charts_user_id_users_id_fk": { + "name": "combined_charts_user_id_users_id_fk", + "tableFrom": "combined_charts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discord_controls": { + "name": "discord_controls", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used": { + "name": "used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "discord_controls_expiry_idx": { + "name": "discord_controls_expiry_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "discord_controls_user_id_users_id_fk": { + "name": "discord_controls_user_id_users_id_fk", + "tableFrom": "discord_controls", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discord_deliveries": { + "name": "discord_deliveries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image_hash": { + "name": "image_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message_url": { + "name": "message_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "discord_deliveries_user_id_users_id_fk": { + "name": "discord_deliveries_user_id_users_id_fk", + "tableFrom": "discord_deliveries", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discord_receipts": { + "name": "discord_receipts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "response": { + "name": "response", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "discord_receipts_expiry_idx": { + "name": "discord_receipts_expiry_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "discord_receipts_user_id_users_id_fk": { + "name": "discord_receipts_user_id_users_id_fk", + "tableFrom": "discord_receipts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "habit_calendar_settings": { + "name": "habit_calendar_settings", + "columns": { + "habit_id": { + "name": "habit_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "settings": { + "name": "settings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "habit_calendar_settings_habit_id_habits_id_fk": { + "name": "habit_calendar_settings_habit_id_habits_id_fk", + "tableFrom": "habit_calendar_settings", + "tableTo": "habits", + "columnsFrom": [ + "habit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "habit_days": { + "name": "habit_days", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "habit_id": { + "name": "habit_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision_id": { + "name": "revision_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ends_at": { + "name": "ends_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count_set": { + "name": "count_set", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "done": { + "name": "done", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "habit_days_lookup_idx": { + "name": "habit_days_lookup_idx", + "columns": [ + "habit_id", + "date", + "revision_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "habit_days_habit_id_habits_id_fk": { + "name": "habit_days_habit_id_habits_id_fk", + "tableFrom": "habit_days", + "tableTo": "habits", + "columnsFrom": [ + "habit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "habit_days_revision_id_habit_revisions_id_fk": { + "name": "habit_days_revision_id_habit_revisions_id_fk", + "tableFrom": "habit_days", + "tableTo": "habit_revisions", + "columnsFrom": [ + "revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "habit_revisions": { + "name": "habit_revisions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "habit_id": { + "name": "habit_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "effective_date": { + "name": "effective_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "habit_revisions_date_idx": { + "name": "habit_revisions_date_idx", + "columns": [ + "habit_id", + "effective_date", + "id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "habit_revisions_habit_id_habits_id_fk": { + "name": "habit_revisions_habit_id_habits_id_fk", + "tableFrom": "habit_revisions", + "tableTo": "habits", + "columnsFrom": [ + "habit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "habits": { + "name": "habits", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_date": { + "name": "created_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "materialized_through": { + "name": "materialized_through", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "calendar_revision": { + "name": "calendar_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "habits_user_idx": { + "name": "habits_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "habits_user_id_users_id_fk": { + "name": "habits_user_id_users_id_fk", + "tableFrom": "habits", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "live_discord_cards": { + "name": "live_discord_cards", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message_url": { + "name": "message_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image_hash": { + "name": "image_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dirty": { + "name": "dirty", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "retry_at": { + "name": "retry_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "live_discord_cards_user_id_users_id_fk": { + "name": "live_discord_cards_user_id_users_id_fk", + "tableFrom": "live_discord_cards", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "progress_events": { + "name": "progress_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "day_id": { + "name": "day_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "occurrence_id": { + "name": "occurrence_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "before": { + "name": "before", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "after": { + "name": "after", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "progress_events_day_idx": { + "name": "progress_events_day_idx", + "columns": [ + "day_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "progress_events_day_id_habit_days_id_fk": { + "name": "progress_events_day_id_habit_days_id_fk", + "tableFrom": "progress_events", + "tableTo": "habit_days", + "columnsFrom": [ + "day_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reminder_deliveries": { + "name": "reminder_deliveries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slot": { + "name": "slot", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'*'" + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retry_at": { + "name": "retry_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "reminder_deliveries_user_date_slot_idx": { + "name": "reminder_deliveries_user_date_slot_idx", + "columns": [ + "user_id", + "date", + "slot" + ], + "isUnique": true + } + }, + "foreignKeys": { + "reminder_deliveries_user_id_users_id_fk": { + "name": "reminder_deliveries_user_id_users_id_fk", + "tableFrom": "reminder_deliveries", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reminder_settings": { + "name": "reminder_settings", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "time": { + "name": "time", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'20:00'" + }, + "reminders": { + "name": "reminders", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quiet_start": { + "name": "quiet_start", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'22:00'" + }, + "quiet_end": { + "name": "quiet_end", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'08:00'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "reminder_settings_user_id_users_id_fk": { + "name": "reminder_settings_user_id_users_id_fk", + "tableFrom": "reminder_settings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reminder_worker_state": { + "name": "reminder_worker_state", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "blocked_until": { + "name": "blocked_until", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sharing_limits": { + "name": "sharing_limits", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "task_occurrences": { + "name": "task_occurrences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "day_id": { + "name": "day_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "done": { + "name": "done", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "expired_at": { + "name": "expired_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "closed_at": { + "name": "closed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "task_occurrences_day_idx": { + "name": "task_occurrences_day_idx", + "columns": [ + "day_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "task_occurrences_day_id_habit_days_id_fk": { + "name": "task_occurrences_day_id_habit_days_id_fk", + "tableFrom": "task_occurrences", + "tableTo": "habit_days", + "columnsFrom": [ + "day_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "discord_id": { + "name": "discord_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "global_name": { + "name": "global_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_hash": { + "name": "avatar_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'UTC'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_discord_id_unique": { + "name": "users_discord_id_unique", + "columns": [ + "discord_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 4dde595..97d62ed 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -92,6 +92,13 @@ "when": 1788847978168, "tag": "0012_discord_commands", "breakpoints": true + }, + { + "idx": 13, + "version": "6", + "when": 1788860000000, + "tag": "0013_habit_calendar_revision", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/src/db/schema.ts b/src/db/schema.ts index 7e2cd37..04ba11c 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -33,6 +33,7 @@ export const discordDeliveries = sqliteTable("discord_deliveries", { // Revisions are append-only, including multiple edits on the same local date. export const habits = sqliteTable('habits', { id: text('id').primaryKey(), userId: text('user_id').notNull().references(() => users.id), + calendarRevision: integer('calendar_revision').notNull().default(0), createdDate: text('created_date').notNull(), materializedThrough: text('materialized_through'), createdAt: integer('created_at').notNull(), }, t => [index('habits_user_idx').on(t.userId)]); export const habitRevisions = sqliteTable('habit_revisions', { diff --git a/src/habits/performance.test.ts b/src/habits/performance.test.ts index e8979dd..c727950 100644 --- a/src/habits/performance.test.ts +++ b/src/habits/performance.test.ts @@ -21,3 +21,26 @@ test('single-day reads load only the selected latest snapshot and its tasks', as expect(logs.find(query => query.includes('task_occurrences'))).toContain('"habit_days"."date" >='); } finally { f.close(); } }); + +test('calendar summaries omit editor payloads and mutation versions cover historical changes', async () => { + const f = fixture(); + try { + const habit = await f.json('/habits', 'POST', { name: 'Tasks', method: 'tasks', tasks: [{name:'Read'}] }, 201); + const first = (await f.json('/today')).habits[0]; + expect((await f.json('/today')).habits[0].calendarRevision).toBe(first.calendarRevision); + f.setTime('2026-09-05T12:00:00Z'); + await f.json(`/habits/${habit.id}/days/2026-09-04/tasks/${first.tasks[0].taskId}`, 'PUT', {done:true}); + const changed = (await f.json('/today')).habits[0].calendarRevision; + expect(changed).toBeGreaterThan(first.calendarRevision); + await f.json(`/habits/${habit.id}/calendar-settings`, 'PUT', {mainColor:'#123456'}); + expect((await f.json('/today')).habits[0].calendarRevision).toBeGreaterThan(changed); + const url = `/habits/${habit.id}/calendar?from=2026-09-04&to=2026-09-05`; + const full = await f.json(url); + const compact = await f.json(url+'&compact=true'); + const detail = full.days[0].habits[0]; + const {tasks, requirements, ...summary} = detail; + expect(compact.days[0].habits[0]).toEqual(summary); + expect(compact.data).toEqual(full.data); + expect((await f.json(`/habits/${habit.id}/days/2026-09-04`)).tasks[0].done).toBe(true); + } finally { f.close(); } +}); diff --git a/src/habits/routes.ts b/src/habits/routes.ts index 61280a8..b8966d6 100644 --- a/src/habits/routes.ts +++ b/src/habits/routes.ts @@ -20,7 +20,7 @@ function date(c: Context) { return result.data; } function range(c: Context) { - const result = z.object({ from: dateSchema, to: dateSchema }).strict().safeParse(c.req.query()); + const result = z.object({ from: dateSchema, to: dateSchema, compact: z.enum(['true', 'false']).optional() }).strict().safeParse(c.req.query()); if (!result.success || result.data.from > result.data.to || dayNumber(result.data.to) - dayNumber(result.data.from) > 1829) throw new ApiError(422, 'Supply from and to dates in order, spanning at most 1830 days'); return result.data; } @@ -80,7 +80,7 @@ export function createHabitRoutes(db: AppDatabase, auth: ReturnType c.json(service(c).settings(id(c)))); app.put('/habits/:id/calendar-settings', c => c.json(service(c).saveSettings(id(c), body(c, calendarSettingsSchema)))); app.get('/habits/:id/calendar', c => { - const { from, to } = range(c); return c.json(service(c).calendar([id(c)], from, to, service(c).settings(id(c)), false)); + const { from, to, compact } = range(c); return c.json(service(c).calendar([id(c)], from, to, service(c).settings(id(c)), false, compact === 'true')); }); app.get('/today', c => { const s = service(c); const habits = s.list().map(h => s.day(h.id, s.today)); @@ -100,7 +100,7 @@ export function createHabitRoutes(db: AppDatabase, auth: ReturnType { service(c).deleteChart(id(c)); return c.body(null, 204); }); app.get('/charts/:id/calendar', c => { - const chart = service(c).chart(id(c)); const { from, to } = range(c); return c.json(service(c).calendar(chart.habitIds, from, to, chart.settings, true)); + const chart = service(c).chart(id(c)); const { from, to, compact } = range(c); return c.json(service(c).calendar(chart.habitIds, from, to, chart.settings, true, compact === 'true')); }); app.get('/charts/:id/days/:date', c => { const chart = service(c).chart(id(c)); const day = date(c); return c.json(service(c).calendar(chart.habitIds, day, day, chart.settings, true).days[0]); diff --git a/src/habits/service.ts b/src/habits/service.ts index 3b2911b..7b291bd 100644 --- a/src/habits/service.ts +++ b/src/habits/service.ts @@ -55,6 +55,10 @@ export class HabitService { this.recordsCache.delete(id); this.occurrencesCache.delete(id); this.countCache.delete(id); this.loadRevisions(id, from, to); } + private changed(id: string) { + this.db.update(habits).set({ calendarRevision: sql`${habits.calendarRevision} + 1` }).where(eq(habits.id, id)).run(); + this.ownedCache.delete(id); + } current(id: string) { const habit = this.owned(id); const revision = this.revision(id)!; const config = revision.config; @@ -62,7 +66,7 @@ export class HabitService { ? [{ code: 'no_tasks', taskId: null, message: 'Add a task to make this habit due.' }] : config.tasks.filter(task => !schedulesOverlap(config.schedule, task.schedule)) .map(task => ({ code: 'task_never_due', taskId: task.id, message: `Task \"${task.name}\" never coincides with the habit schedule.` })); - return { id: habit.id, ...config, warnings, createdDate: habit.createdDate, createdAt: habit.createdAt, revisionId: revision.id, effectiveDate: revision.effectiveDate }; + return { id: habit.id, calendarRevision: habit.calendarRevision, ...config, warnings, createdDate: habit.createdDate, createdAt: habit.createdAt, revisionId: revision.id, effectiveDate: revision.effectiveDate }; } list(includeArchived = false) { return this.db.select().from(habits).where(eq(habits.userId, this.user.id)).orderBy(asc(habits.createdAt), asc(habits.id)).all() @@ -130,6 +134,7 @@ export class HabitService { this.db.transaction(() => { this.db.insert(habitRevisions).values({ habitId: id, effectiveDate: this.today, config, createdAt: this.now }).run(); this.materialize(this.owned(id)); + this.changed(id); }); return this.current(id); } @@ -270,7 +275,7 @@ export class HabitService { const value = !due ? 0 : config!.method === 'count' ? counted?.value ?? 0 : config!.method === 'manual' ? Number(record?.done ?? false) : tasks.filter(t => t.done).length; const target = !config ? null : config.method === 'count' ? config.target : config.method === 'manual' ? 1 : tasks.length; const ratio = due ? Math.min(1, value / target!) : null; - return { date, habitId: id, name: config?.name ?? null, method: config?.method ?? null, revisionId: revision?.id ?? null, + return { date, habitId: id, calendarRevision: this.owned(id).calendarRevision, name: config?.name ?? null, method: config?.method ?? null, revisionId: revision?.id ?? null, timezone: record?.timezone ?? this.user.timezone, endsAt: record?.endsAt ?? null, due, future, status: future ? 'future' : !due ? 'not_due' : ratio === 1 ? 'complete' : value > 0 ? 'partial' : 'empty', value, target, carriedFrom: counted?.carriedFrom ?? null, loggedCount: record?.countSet ? record.count : null, unit: config?.method === 'count' ? config.unit : config?.method === 'tasks' ? 'tasks' : 'completion', @@ -291,6 +296,7 @@ export class HabitService { this.db.transaction(() => { this.db.insert(progressEvents).values({ dayId: record.id, before: { count: detail.value, done: record.done, loggedCount: record.countSet ? record.count : null, carriedFrom: detail.carriedFrom }, after: input, createdAt: this.now }).run(); this.db.update(habitDays).set('count' in input ? { ...input, countSet: true } : input).where(eq(habitDays.id, record.id)).run(); + this.changed(id); }); this.invalidate(id); return this.day(id, date); @@ -302,6 +308,7 @@ export class HabitService { this.db.transaction(() => { this.db.insert(progressEvents).values({ dayId: record.id, occurrenceId: occurrence.id, before: { done: occurrence.done }, after: { done }, createdAt: this.now }).run(); this.db.update(taskOccurrences).set({ done, updatedAt: this.now }).where(eq(taskOccurrences.id, occurrence.id)).run(); + this.changed(id); }); this.invalidate(id); return this.day(id, date); @@ -325,6 +332,7 @@ export class HabitService { saveSettings(id: string, settings: CalendarSettings) { this.owned(id); this.db.insert(habitCalendarSettings).values({ habitId: id, settings }).onConflictDoUpdate({ target: habitCalendarSettings.habitId, set: { settings } }).run(); + this.changed(id); return settings; } chart(id: string) { @@ -341,7 +349,7 @@ export class HabitService { return this.chart(id); } deleteChart(id: string) { this.chart(id); this.db.delete(combinedCharts).where(eq(combinedCharts.id, id)).run(); } - calendar(ids: string[], from: string, to: string, settings: CalendarSettings, combined: boolean) { + calendar(ids: string[], from: string, to: string, settings: CalendarSettings, combined: boolean, compact = false) { ids.forEach(id => { this.owned(id); this.range(id, from, to); }); const days = []; for (let date = from; date <= to; date = addDays(date, 1)) { @@ -353,7 +361,10 @@ export class HabitService { const progress = shade(ratio, steps, settings, !combined && (settings.shadeCount === 'auto' || manual)); const future = date > this.today; days.push({ date, due: due.length, completed, ratio, future, status: future ? 'future' : ratio === null ? 'not_due' : ratio === 1 ? 'complete' : ratio > 0 ? 'partial' : 'empty', - shadeCount: steps, ...progress, color: future ? settings.futureColor : progress.color, habits: details }); + shadeCount: steps, ...progress, color: future ? settings.futureColor : progress.color, habits: details.map(detail => { + const { tasks, requirements, ...summary } = detail; + return { ...summary, tasks: compact ? undefined : tasks, requirements: compact ? undefined : requirements }; + }) }); } return { kind: combined ? 'combined' as const : 'individual' as const, from, to, today: this.today, timezone: this.user.timezone, settings, days, // Explicit colors and details are supplied for the client's Nivo colorScale/tooltip adapters.