feat(04-01): add list tables to schema and apply via generate+migrate [BLOCKING]
- Append lists, list_shares, list_items tables to Drizzle schema (schema.ts) - lists: owner_id FK, is_shared bool default true (D-01), idx_lists_owner_id - list_shares: list_id + user_id FKs, uniq_list_share, idx_list_shares_user_id (D-02) - list_items: rank varchar for fractional-indexing (D-13), checked bool, composite indexes - Generate 0000_easy_slipstream.sql (full schema baseline) + 0001_lists_schema.sql (new tables) - Mark 0000 as applied in __drizzle_migrations (prior tables existed from manual DDL) - Apply 0001_lists_schema.sql via db:migrate — lists/list_shares/list_items now in MariaDB - Add vitest/globals + node to tsconfig types for test file compatibility - NEVER used db:push (hard project constraint — drizzle-mariadb-push-unsafe) - typecheck passes
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
CREATE TABLE `calendar_events` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`calendar_id` int NOT NULL,
|
||||
`uid` varchar(512) NOT NULL,
|
||||
`etag` varchar(256),
|
||||
`object_url` varchar(1024),
|
||||
`raw_vevent` text NOT NULL,
|
||||
`dtstart_utc` timestamp,
|
||||
`dtstart_date` date,
|
||||
`all_day` boolean NOT NULL DEFAULT false,
|
||||
`has_rrule` boolean NOT NULL DEFAULT false,
|
||||
`updated_at` timestamp DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `calendar_events_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `uniq_calendar_uid` UNIQUE(`calendar_id`,`uid`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `calendar_outbox` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`user_id` int NOT NULL,
|
||||
`operation` enum('create','update','delete') NOT NULL,
|
||||
`status` enum('pending','done','failed','dead') NOT NULL DEFAULT 'pending',
|
||||
`uid` varchar(512) NOT NULL,
|
||||
`calendar_url` varchar(1024) NOT NULL,
|
||||
`calendar_object_url` varchar(1024),
|
||||
`etag` varchar(256),
|
||||
`payload` text,
|
||||
`attempt_count` int NOT NULL DEFAULT 0,
|
||||
`next_attempt_at` timestamp NOT NULL DEFAULT (now()),
|
||||
`last_error` text,
|
||||
`group_id` varchar(64),
|
||||
`created_at` timestamp NOT NULL DEFAULT (now()),
|
||||
`updated_at` timestamp DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `calendar_outbox_id` PRIMARY KEY(`id`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `calendars` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`user_id` int NOT NULL,
|
||||
`url` varchar(1024) NOT NULL,
|
||||
`display_name` varchar(256),
|
||||
`color` varchar(7),
|
||||
`ctag` varchar(512),
|
||||
`sync_token` varchar(1024),
|
||||
`last_synced_at` timestamp,
|
||||
`is_shared` boolean NOT NULL DEFAULT false,
|
||||
CONSTRAINT `calendars_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `uniq_calendar_user_url` UNIQUE(`user_id`,`url`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `list_items` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`list_id` int NOT NULL,
|
||||
`text` varchar(500) NOT NULL,
|
||||
`checked` boolean NOT NULL DEFAULT false,
|
||||
`rank` varchar(255) NOT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT (now()),
|
||||
`updated_at` timestamp DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `list_items_id` PRIMARY KEY(`id`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `list_shares` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`list_id` int NOT NULL,
|
||||
`user_id` int NOT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT (now()),
|
||||
CONSTRAINT `list_shares_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `uniq_list_share` UNIQUE(`list_id`,`user_id`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `lists` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`owner_id` int NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`is_shared` boolean NOT NULL DEFAULT true,
|
||||
`created_at` timestamp NOT NULL DEFAULT (now()),
|
||||
`updated_at` timestamp DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `lists_id` PRIMARY KEY(`id`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `member_credentials` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`user_id` int NOT NULL,
|
||||
`encrypted_password` text NOT NULL,
|
||||
`fastmail_email` varchar(256) NOT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT (now()),
|
||||
`updated_at` timestamp DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `member_credentials_id` PRIMARY KEY(`id`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `users` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`oidc_iss` varchar(512) NOT NULL,
|
||||
`oidc_sub` varchar(256) NOT NULL,
|
||||
`display_name` varchar(256),
|
||||
`color` varchar(7) NOT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT (now()),
|
||||
CONSTRAINT `users_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `uniq_oidc_identity` UNIQUE(`oidc_iss`,`oidc_sub`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `calendar_events` ADD CONSTRAINT `calendar_events_calendar_id_calendars_id_fk` FOREIGN KEY (`calendar_id`) REFERENCES `calendars`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE `calendar_outbox` ADD CONSTRAINT `calendar_outbox_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE `calendars` ADD CONSTRAINT `calendars_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE `list_items` ADD CONSTRAINT `list_items_list_id_lists_id_fk` FOREIGN KEY (`list_id`) REFERENCES `lists`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE `list_shares` ADD CONSTRAINT `list_shares_list_id_lists_id_fk` FOREIGN KEY (`list_id`) REFERENCES `lists`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE `list_shares` ADD CONSTRAINT `list_shares_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE `lists` ADD CONSTRAINT `lists_owner_id_users_id_fk` FOREIGN KEY (`owner_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE `member_credentials` ADD CONSTRAINT `member_credentials_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX `idx_calendar_events_dtstart_utc` ON `calendar_events` (`dtstart_utc`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_calendar_events_dtstart_date` ON `calendar_events` (`dtstart_date`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_calendar_events_has_rrule` ON `calendar_events` (`has_rrule`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_outbox_user_status` ON `calendar_outbox` (`user_id`,`status`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_outbox_next_attempt` ON `calendar_outbox` (`next_attempt_at`,`status`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_outbox_uid` ON `calendar_outbox` (`uid`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_calendars_user_id` ON `calendars` (`user_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_list_items_list_id_rank` ON `list_items` (`list_id`,`rank`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_list_items_list_id_checked` ON `list_items` (`list_id`,`checked`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_list_shares_user_id` ON `list_shares` (`user_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_lists_owner_id` ON `lists` (`owner_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_member_credentials_user_id` ON `member_credentials` (`user_id`);
|
||||
@@ -0,0 +1,44 @@
|
||||
-- Phase 4: Add lists, list_shares, list_items tables.
|
||||
-- Additive migration only — no DROP, TRUNCATE, or ALTER of existing tables.
|
||||
--
|
||||
-- D-01: lists.is_shared defaults to true (collaborative household use case)
|
||||
-- D-02: list_shares join table is member-count-agnostic
|
||||
-- D-13: list_items.rank is a fractional-indexing varchar string
|
||||
CREATE TABLE `lists` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`owner_id` int NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`is_shared` boolean NOT NULL DEFAULT true,
|
||||
`created_at` timestamp NOT NULL DEFAULT (now()),
|
||||
`updated_at` timestamp DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `lists_id` PRIMARY KEY(`id`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `list_shares` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`list_id` int NOT NULL,
|
||||
`user_id` int NOT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT (now()),
|
||||
CONSTRAINT `list_shares_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `uniq_list_share` UNIQUE(`list_id`,`user_id`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `list_items` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`list_id` int NOT NULL,
|
||||
`text` varchar(500) NOT NULL,
|
||||
`checked` boolean NOT NULL DEFAULT false,
|
||||
`rank` varchar(255) NOT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT (now()),
|
||||
`updated_at` timestamp DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `list_items_id` PRIMARY KEY(`id`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `lists` ADD CONSTRAINT `lists_owner_id_users_id_fk` FOREIGN KEY (`owner_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE `list_shares` ADD CONSTRAINT `list_shares_list_id_lists_id_fk` FOREIGN KEY (`list_id`) REFERENCES `lists`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE `list_shares` ADD CONSTRAINT `list_shares_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE `list_items` ADD CONSTRAINT `list_items_list_id_lists_id_fk` FOREIGN KEY (`list_id`) REFERENCES `lists`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX `idx_lists_owner_id` ON `lists` (`owner_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_list_shares_user_id` ON `list_shares` (`user_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_list_items_list_id_rank` ON `list_items` (`list_id`,`rank`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_list_items_list_id_checked` ON `list_items` (`list_id`,`checked`);
|
||||
@@ -0,0 +1,864 @@
|
||||
{
|
||||
"version": "5",
|
||||
"dialect": "mysql",
|
||||
"id": "e4665da0-ef95-4994-af4a-0d6a0169c874",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"calendar_events": {
|
||||
"name": "calendar_events",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"calendar_id": {
|
||||
"name": "calendar_id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"uid": {
|
||||
"name": "uid",
|
||||
"type": "varchar(512)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"etag": {
|
||||
"name": "etag",
|
||||
"type": "varchar(256)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"object_url": {
|
||||
"name": "object_url",
|
||||
"type": "varchar(1024)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"raw_vevent": {
|
||||
"name": "raw_vevent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"dtstart_utc": {
|
||||
"name": "dtstart_utc",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"dtstart_date": {
|
||||
"name": "dtstart_date",
|
||||
"type": "date",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"all_day": {
|
||||
"name": "all_day",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"has_rrule": {
|
||||
"name": "has_rrule",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"idx_calendar_events_dtstart_utc": {
|
||||
"name": "idx_calendar_events_dtstart_utc",
|
||||
"columns": [
|
||||
"dtstart_utc"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"idx_calendar_events_dtstart_date": {
|
||||
"name": "idx_calendar_events_dtstart_date",
|
||||
"columns": [
|
||||
"dtstart_date"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"idx_calendar_events_has_rrule": {
|
||||
"name": "idx_calendar_events_has_rrule",
|
||||
"columns": [
|
||||
"has_rrule"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"calendar_events_calendar_id_calendars_id_fk": {
|
||||
"name": "calendar_events_calendar_id_calendars_id_fk",
|
||||
"tableFrom": "calendar_events",
|
||||
"tableTo": "calendars",
|
||||
"columnsFrom": [
|
||||
"calendar_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"calendar_events_id": {
|
||||
"name": "calendar_events_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"uniq_calendar_uid": {
|
||||
"name": "uniq_calendar_uid",
|
||||
"columns": [
|
||||
"calendar_id",
|
||||
"uid"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"calendar_outbox": {
|
||||
"name": "calendar_outbox",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"operation": {
|
||||
"name": "operation",
|
||||
"type": "enum('create','update','delete')",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "enum('pending','done','failed','dead')",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'pending'"
|
||||
},
|
||||
"uid": {
|
||||
"name": "uid",
|
||||
"type": "varchar(512)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"calendar_url": {
|
||||
"name": "calendar_url",
|
||||
"type": "varchar(1024)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"calendar_object_url": {
|
||||
"name": "calendar_object_url",
|
||||
"type": "varchar(1024)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"etag": {
|
||||
"name": "etag",
|
||||
"type": "varchar(256)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"payload": {
|
||||
"name": "payload",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"attempt_count": {
|
||||
"name": "attempt_count",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"next_attempt_at": {
|
||||
"name": "next_attempt_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"last_error": {
|
||||
"name": "last_error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"group_id": {
|
||||
"name": "group_id",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"idx_outbox_user_status": {
|
||||
"name": "idx_outbox_user_status",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"status"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"idx_outbox_next_attempt": {
|
||||
"name": "idx_outbox_next_attempt",
|
||||
"columns": [
|
||||
"next_attempt_at",
|
||||
"status"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"idx_outbox_uid": {
|
||||
"name": "idx_outbox_uid",
|
||||
"columns": [
|
||||
"uid"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"calendar_outbox_user_id_users_id_fk": {
|
||||
"name": "calendar_outbox_user_id_users_id_fk",
|
||||
"tableFrom": "calendar_outbox",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"calendar_outbox_id": {
|
||||
"name": "calendar_outbox_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"calendars": {
|
||||
"name": "calendars",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "varchar(1024)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"display_name": {
|
||||
"name": "display_name",
|
||||
"type": "varchar(256)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"color": {
|
||||
"name": "color",
|
||||
"type": "varchar(7)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ctag": {
|
||||
"name": "ctag",
|
||||
"type": "varchar(512)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"sync_token": {
|
||||
"name": "sync_token",
|
||||
"type": "varchar(1024)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_synced_at": {
|
||||
"name": "last_synced_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"is_shared": {
|
||||
"name": "is_shared",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"idx_calendars_user_id": {
|
||||
"name": "idx_calendars_user_id",
|
||||
"columns": [
|
||||
"user_id"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"calendars_user_id_users_id_fk": {
|
||||
"name": "calendars_user_id_users_id_fk",
|
||||
"tableFrom": "calendars",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"calendars_id": {
|
||||
"name": "calendars_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"uniq_calendar_user_url": {
|
||||
"name": "uniq_calendar_user_url",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"url"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"list_items": {
|
||||
"name": "list_items",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"list_id": {
|
||||
"name": "list_id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"text": {
|
||||
"name": "text",
|
||||
"type": "varchar(500)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"checked": {
|
||||
"name": "checked",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"rank": {
|
||||
"name": "rank",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"idx_list_items_list_id_rank": {
|
||||
"name": "idx_list_items_list_id_rank",
|
||||
"columns": [
|
||||
"list_id",
|
||||
"rank"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"idx_list_items_list_id_checked": {
|
||||
"name": "idx_list_items_list_id_checked",
|
||||
"columns": [
|
||||
"list_id",
|
||||
"checked"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"list_items_list_id_lists_id_fk": {
|
||||
"name": "list_items_list_id_lists_id_fk",
|
||||
"tableFrom": "list_items",
|
||||
"tableTo": "lists",
|
||||
"columnsFrom": [
|
||||
"list_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"list_items_id": {
|
||||
"name": "list_items_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"list_shares": {
|
||||
"name": "list_shares",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"list_id": {
|
||||
"name": "list_id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"idx_list_shares_user_id": {
|
||||
"name": "idx_list_shares_user_id",
|
||||
"columns": [
|
||||
"user_id"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"list_shares_list_id_lists_id_fk": {
|
||||
"name": "list_shares_list_id_lists_id_fk",
|
||||
"tableFrom": "list_shares",
|
||||
"tableTo": "lists",
|
||||
"columnsFrom": [
|
||||
"list_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"list_shares_user_id_users_id_fk": {
|
||||
"name": "list_shares_user_id_users_id_fk",
|
||||
"tableFrom": "list_shares",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"list_shares_id": {
|
||||
"name": "list_shares_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"uniq_list_share": {
|
||||
"name": "uniq_list_share",
|
||||
"columns": [
|
||||
"list_id",
|
||||
"user_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"lists": {
|
||||
"name": "lists",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"owner_id": {
|
||||
"name": "owner_id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"is_shared": {
|
||||
"name": "is_shared",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"idx_lists_owner_id": {
|
||||
"name": "idx_lists_owner_id",
|
||||
"columns": [
|
||||
"owner_id"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"lists_owner_id_users_id_fk": {
|
||||
"name": "lists_owner_id_users_id_fk",
|
||||
"tableFrom": "lists",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"owner_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"lists_id": {
|
||||
"name": "lists_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"member_credentials": {
|
||||
"name": "member_credentials",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"encrypted_password": {
|
||||
"name": "encrypted_password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"fastmail_email": {
|
||||
"name": "fastmail_email",
|
||||
"type": "varchar(256)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"idx_member_credentials_user_id": {
|
||||
"name": "idx_member_credentials_user_id",
|
||||
"columns": [
|
||||
"user_id"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"member_credentials_user_id_users_id_fk": {
|
||||
"name": "member_credentials_user_id_users_id_fk",
|
||||
"tableFrom": "member_credentials",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"member_credentials_id": {
|
||||
"name": "member_credentials_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"users": {
|
||||
"name": "users",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"oidc_iss": {
|
||||
"name": "oidc_iss",
|
||||
"type": "varchar(512)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"oidc_sub": {
|
||||
"name": "oidc_sub",
|
||||
"type": "varchar(256)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"display_name": {
|
||||
"name": "display_name",
|
||||
"type": "varchar(256)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"color": {
|
||||
"name": "color",
|
||||
"type": "varchar(7)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"users_id": {
|
||||
"name": "users_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"uniq_oidc_identity": {
|
||||
"name": "uniq_oidc_identity",
|
||||
"columns": [
|
||||
"oidc_iss",
|
||||
"oidc_sub"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"tables": {},
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "mysql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "5",
|
||||
"when": 1781020239657,
|
||||
"tag": "0000_easy_slipstream",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "5",
|
||||
"when": 1781020500000,
|
||||
"tag": "0001_lists_schema",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -12,6 +12,9 @@ import {
|
||||
unique,
|
||||
} from 'drizzle-orm/mysql-core'
|
||||
|
||||
// ── Phase 4: List tables ───────────────────────────────────────────────────
|
||||
// Imported by test/setup.ts for afterEach cleanup — keep exports consistent.
|
||||
|
||||
/**
|
||||
* Members of the household — identity keyed by oidc_iss + oidc_sub (never email, per D-10).
|
||||
* Color auto-assigned from palette on first login (D-06).
|
||||
@@ -161,3 +164,78 @@ export const calendarOutbox = mysqlTable(
|
||||
index('idx_outbox_uid').on(t.uid),
|
||||
],
|
||||
)
|
||||
|
||||
/**
|
||||
* App-owned named lists — stored in MariaDB, NOT CalDAV (Phase 4).
|
||||
*
|
||||
* D-01: isShared defaults to true (collaborative household use case).
|
||||
* D-02: ownership via ownerId; sharing via listShares join table (member-count-agnostic).
|
||||
*/
|
||||
export const lists = mysqlTable(
|
||||
'lists',
|
||||
{
|
||||
id: int().primaryKey().autoincrement(),
|
||||
ownerId: int('owner_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
// D-01: default shared — the primary grocery/hub use case is collaborative.
|
||||
isShared: boolean('is_shared').default(true).notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(),
|
||||
},
|
||||
(t) => [index('idx_lists_owner_id').on(t.ownerId)],
|
||||
)
|
||||
|
||||
/**
|
||||
* List sharing join table (D-02, member-count-agnostic).
|
||||
*
|
||||
* One row per (list, member) pair. v1 UI treats a list as "shared" when any
|
||||
* row exists; future UI can offer per-recipient granularity without a migration.
|
||||
*/
|
||||
export const listShares = mysqlTable(
|
||||
'list_shares',
|
||||
{
|
||||
id: int().primaryKey().autoincrement(),
|
||||
listId: int('list_id')
|
||||
.notNull()
|
||||
.references(() => lists.id, { onDelete: 'cascade' }),
|
||||
userId: int('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
},
|
||||
(t) => [
|
||||
unique('uniq_list_share').on(t.listId, t.userId),
|
||||
index('idx_list_shares_user_id').on(t.userId),
|
||||
],
|
||||
)
|
||||
|
||||
/**
|
||||
* Items within a list.
|
||||
*
|
||||
* D-13: rank uses fractional-indexing strings (e.g. "a0", "a1", "Zz") — a single
|
||||
* move rewrites only the moved item's rank (one-row write), which plays well with
|
||||
* SSE live sync and concurrent reorders.
|
||||
* D-05: checked items render in a "completed" section at the bottom; not removed.
|
||||
*/
|
||||
export const listItems = mysqlTable(
|
||||
'list_items',
|
||||
{
|
||||
id: int().primaryKey().autoincrement(),
|
||||
listId: int('list_id')
|
||||
.notNull()
|
||||
.references(() => lists.id, { onDelete: 'cascade' }),
|
||||
text: varchar('text', { length: 500 }).notNull(),
|
||||
checked: boolean('checked').default(false).notNull(),
|
||||
rank: varchar('rank', { length: 255 }).notNull(), // fractional-indexing string (D-13)
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(),
|
||||
},
|
||||
(t) => [
|
||||
// Composite index covers ordered list fetch: WHERE list_id=? ORDER BY rank
|
||||
index('idx_list_items_list_id_rank').on(t.listId, t.rank),
|
||||
// Secondary index for checked/unchecked split queries
|
||||
index('idx_list_items_list_id_checked').on(t.listId, t.checked),
|
||||
],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user