aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--drizzle/0002_married_moondragon.sql54
-rw-r--r--drizzle/meta/0002_snapshot.json837
-rw-r--r--drizzle/meta/_journal.json7
-rw-r--r--src/server/db/schema.ts83
-rw-r--r--src/server/schemas/index.ts107
-rw-r--r--src/server/schemas/note.test.ts427
6 files changed, 1515 insertions, 0 deletions
diff --git a/drizzle/0002_married_moondragon.sql b/drizzle/0002_married_moondragon.sql
new file mode 100644
index 0000000..dedd21a
--- /dev/null
+++ b/drizzle/0002_married_moondragon.sql
@@ -0,0 +1,54 @@
+CREATE TABLE "note_field_types" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "note_type_id" uuid NOT NULL,
+ "name" varchar(255) NOT NULL,
+ "order" integer NOT NULL,
+ "field_type" varchar(50) DEFAULT 'text' NOT NULL,
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "deleted_at" timestamp with time zone,
+ "sync_version" integer DEFAULT 0 NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "note_field_values" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "note_id" uuid NOT NULL,
+ "note_field_type_id" uuid NOT NULL,
+ "value" text NOT NULL,
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "sync_version" integer DEFAULT 0 NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "note_types" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "user_id" uuid NOT NULL,
+ "name" varchar(255) NOT NULL,
+ "front_template" text NOT NULL,
+ "back_template" text NOT NULL,
+ "is_reversible" boolean DEFAULT false NOT NULL,
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "deleted_at" timestamp with time zone,
+ "sync_version" integer DEFAULT 0 NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "notes" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "deck_id" uuid NOT NULL,
+ "note_type_id" uuid NOT NULL,
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "deleted_at" timestamp with time zone,
+ "sync_version" integer DEFAULT 0 NOT NULL
+);
+--> statement-breakpoint
+ALTER TABLE "cards" ADD COLUMN "note_id" uuid;--> statement-breakpoint
+ALTER TABLE "cards" ADD COLUMN "is_reversed" boolean;--> statement-breakpoint
+ALTER TABLE "note_field_types" ADD CONSTRAINT "note_field_types_note_type_id_note_types_id_fk" FOREIGN KEY ("note_type_id") REFERENCES "public"."note_types"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "note_field_values" ADD CONSTRAINT "note_field_values_note_id_notes_id_fk" FOREIGN KEY ("note_id") REFERENCES "public"."notes"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "note_field_values" ADD CONSTRAINT "note_field_values_note_field_type_id_note_field_types_id_fk" FOREIGN KEY ("note_field_type_id") REFERENCES "public"."note_field_types"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "note_types" ADD CONSTRAINT "note_types_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "notes" ADD CONSTRAINT "notes_deck_id_decks_id_fk" FOREIGN KEY ("deck_id") REFERENCES "public"."decks"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "notes" ADD CONSTRAINT "notes_note_type_id_note_types_id_fk" FOREIGN KEY ("note_type_id") REFERENCES "public"."note_types"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "cards" ADD CONSTRAINT "cards_note_id_notes_id_fk" FOREIGN KEY ("note_id") REFERENCES "public"."notes"("id") ON DELETE no action ON UPDATE no action; \ No newline at end of file
diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json
new file mode 100644
index 0000000..acf7f37
--- /dev/null
+++ b/drizzle/meta/0002_snapshot.json
@@ -0,0 +1,837 @@
+{
+ "id": "4b30f43f-1827-40c6-9814-d749334da729",
+ "prevId": "d3d0fb16-7e44-4217-916e-a5edc9ab7a16",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.cards": {
+ "name": "cards",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "deck_id": {
+ "name": "deck_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "note_id": {
+ "name": "note_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_reversed": {
+ "name": "is_reversed",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "front": {
+ "name": "front",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "back": {
+ "name": "back",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state": {
+ "name": "state",
+ "type": "smallint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "due": {
+ "name": "due",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "stability": {
+ "name": "stability",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "difficulty": {
+ "name": "difficulty",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "elapsed_days": {
+ "name": "elapsed_days",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "scheduled_days": {
+ "name": "scheduled_days",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "reps": {
+ "name": "reps",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "lapses": {
+ "name": "lapses",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_review": {
+ "name": "last_review",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_version": {
+ "name": "sync_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "cards_deck_id_decks_id_fk": {
+ "name": "cards_deck_id_decks_id_fk",
+ "tableFrom": "cards",
+ "tableTo": "decks",
+ "columnsFrom": [
+ "deck_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "cards_note_id_notes_id_fk": {
+ "name": "cards_note_id_notes_id_fk",
+ "tableFrom": "cards",
+ "tableTo": "notes",
+ "columnsFrom": [
+ "note_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.decks": {
+ "name": "decks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "new_cards_per_day": {
+ "name": "new_cards_per_day",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 20
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_version": {
+ "name": "sync_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "decks_user_id_users_id_fk": {
+ "name": "decks_user_id_users_id_fk",
+ "tableFrom": "decks",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.note_field_types": {
+ "name": "note_field_types",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "note_type_id": {
+ "name": "note_type_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "field_type": {
+ "name": "field_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'text'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_version": {
+ "name": "sync_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "note_field_types_note_type_id_note_types_id_fk": {
+ "name": "note_field_types_note_type_id_note_types_id_fk",
+ "tableFrom": "note_field_types",
+ "tableTo": "note_types",
+ "columnsFrom": [
+ "note_type_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.note_field_values": {
+ "name": "note_field_values",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "note_id": {
+ "name": "note_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "note_field_type_id": {
+ "name": "note_field_type_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "sync_version": {
+ "name": "sync_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "note_field_values_note_id_notes_id_fk": {
+ "name": "note_field_values_note_id_notes_id_fk",
+ "tableFrom": "note_field_values",
+ "tableTo": "notes",
+ "columnsFrom": [
+ "note_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "note_field_values_note_field_type_id_note_field_types_id_fk": {
+ "name": "note_field_values_note_field_type_id_note_field_types_id_fk",
+ "tableFrom": "note_field_values",
+ "tableTo": "note_field_types",
+ "columnsFrom": [
+ "note_field_type_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.note_types": {
+ "name": "note_types",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "front_template": {
+ "name": "front_template",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "back_template": {
+ "name": "back_template",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_reversible": {
+ "name": "is_reversible",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_version": {
+ "name": "sync_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "note_types_user_id_users_id_fk": {
+ "name": "note_types_user_id_users_id_fk",
+ "tableFrom": "note_types",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.notes": {
+ "name": "notes",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "deck_id": {
+ "name": "deck_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "note_type_id": {
+ "name": "note_type_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_version": {
+ "name": "sync_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "notes_deck_id_decks_id_fk": {
+ "name": "notes_deck_id_decks_id_fk",
+ "tableFrom": "notes",
+ "tableTo": "decks",
+ "columnsFrom": [
+ "deck_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "notes_note_type_id_note_types_id_fk": {
+ "name": "notes_note_type_id_note_types_id_fk",
+ "tableFrom": "notes",
+ "tableTo": "note_types",
+ "columnsFrom": [
+ "note_type_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.refresh_tokens": {
+ "name": "refresh_tokens",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_hash": {
+ "name": "token_hash",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "refresh_tokens_user_id_users_id_fk": {
+ "name": "refresh_tokens_user_id_users_id_fk",
+ "tableFrom": "refresh_tokens",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.review_logs": {
+ "name": "review_logs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "card_id": {
+ "name": "card_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "rating": {
+ "name": "rating",
+ "type": "smallint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state": {
+ "name": "state",
+ "type": "smallint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scheduled_days": {
+ "name": "scheduled_days",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "elapsed_days": {
+ "name": "elapsed_days",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reviewed_at": {
+ "name": "reviewed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "duration_ms": {
+ "name": "duration_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_version": {
+ "name": "sync_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "review_logs_card_id_cards_id_fk": {
+ "name": "review_logs_card_id_cards_id_fk",
+ "tableFrom": "review_logs",
+ "tableTo": "cards",
+ "columnsFrom": [
+ "card_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "review_logs_user_id_users_id_fk": {
+ "name": "review_logs_user_id_users_id_fk",
+ "tableFrom": "review_logs",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.users": {
+ "name": "users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "username": {
+ "name": "username",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "users_username_unique": {
+ "name": "users_username_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "username"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+} \ No newline at end of file
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index f245fa6..5da2908 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -15,6 +15,13 @@
"when": 1764708169736,
"tag": "0001_spotty_jane_foster",
"breakpoints": true
+ },
+ {
+ "idx": 2,
+ "version": "7",
+ "when": 1767108756805,
+ "tag": "0002_married_moondragon",
+ "breakpoints": true
}
]
} \ No newline at end of file
diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts
index 4b9631f..bd3d396 100644
--- a/src/server/db/schema.ts
+++ b/src/server/db/schema.ts
@@ -1,4 +1,5 @@
import {
+ boolean,
integer,
pgTable,
real,
@@ -25,6 +26,11 @@ export const Rating = {
Easy: 4,
} as const;
+// Field types for note fields
+export const FieldType = {
+ Text: "text",
+} as const;
+
export const users = pgTable("users", {
id: uuid("id").primaryKey().defaultRandom(),
username: varchar("username", { length: 255 }).notNull().unique(),
@@ -49,6 +55,45 @@ export const refreshTokens = pgTable("refresh_tokens", {
.defaultNow(),
});
+export const noteTypes = pgTable("note_types", {
+ id: uuid("id").primaryKey().defaultRandom(),
+ userId: uuid("user_id")
+ .notNull()
+ .references(() => users.id),
+ name: varchar("name", { length: 255 }).notNull(),
+ frontTemplate: text("front_template").notNull(),
+ backTemplate: text("back_template").notNull(),
+ isReversible: boolean("is_reversible").notNull().default(false),
+ createdAt: timestamp("created_at", { withTimezone: true })
+ .notNull()
+ .defaultNow(),
+ updatedAt: timestamp("updated_at", { withTimezone: true })
+ .notNull()
+ .defaultNow(),
+ deletedAt: timestamp("deleted_at", { withTimezone: true }),
+ syncVersion: integer("sync_version").notNull().default(0),
+});
+
+export const noteFieldTypes = pgTable("note_field_types", {
+ id: uuid("id").primaryKey().defaultRandom(),
+ noteTypeId: uuid("note_type_id")
+ .notNull()
+ .references(() => noteTypes.id),
+ name: varchar("name", { length: 255 }).notNull(),
+ order: integer("order").notNull(),
+ fieldType: varchar("field_type", { length: 50 })
+ .notNull()
+ .default(FieldType.Text),
+ createdAt: timestamp("created_at", { withTimezone: true })
+ .notNull()
+ .defaultNow(),
+ updatedAt: timestamp("updated_at", { withTimezone: true })
+ .notNull()
+ .defaultNow(),
+ deletedAt: timestamp("deleted_at", { withTimezone: true }),
+ syncVersion: integer("sync_version").notNull().default(0),
+});
+
export const decks = pgTable("decks", {
id: uuid("id").primaryKey().defaultRandom(),
userId: uuid("user_id")
@@ -67,11 +112,49 @@ export const decks = pgTable("decks", {
syncVersion: integer("sync_version").notNull().default(0),
});
+export const notes = pgTable("notes", {
+ id: uuid("id").primaryKey().defaultRandom(),
+ deckId: uuid("deck_id")
+ .notNull()
+ .references(() => decks.id),
+ noteTypeId: uuid("note_type_id")
+ .notNull()
+ .references(() => noteTypes.id),
+ createdAt: timestamp("created_at", { withTimezone: true })
+ .notNull()
+ .defaultNow(),
+ updatedAt: timestamp("updated_at", { withTimezone: true })
+ .notNull()
+ .defaultNow(),
+ deletedAt: timestamp("deleted_at", { withTimezone: true }),
+ syncVersion: integer("sync_version").notNull().default(0),
+});
+
+export const noteFieldValues = pgTable("note_field_values", {
+ id: uuid("id").primaryKey().defaultRandom(),
+ noteId: uuid("note_id")
+ .notNull()
+ .references(() => notes.id),
+ noteFieldTypeId: uuid("note_field_type_id")
+ .notNull()
+ .references(() => noteFieldTypes.id),
+ value: text("value").notNull(),
+ createdAt: timestamp("created_at", { withTimezone: true })
+ .notNull()
+ .defaultNow(),
+ updatedAt: timestamp("updated_at", { withTimezone: true })
+ .notNull()
+ .defaultNow(),
+ syncVersion: integer("sync_version").notNull().default(0),
+});
+
export const cards = pgTable("cards", {
id: uuid("id").primaryKey().defaultRandom(),
deckId: uuid("deck_id")
.notNull()
.references(() => decks.id),
+ noteId: uuid("note_id").references(() => notes.id),
+ isReversed: boolean("is_reversed"),
front: text("front").notNull(),
back: text("back").notNull(),
diff --git a/src/server/schemas/index.ts b/src/server/schemas/index.ts
index 0227ebe..d2a8fb0 100644
--- a/src/server/schemas/index.ts
+++ b/src/server/schemas/index.ts
@@ -105,6 +105,98 @@ export const updateCardSchema = z.object({
back: z.string().min(1).optional(),
});
+// Field type schema
+export const fieldTypeSchema = z.literal("text");
+
+// NoteType schema
+export const noteTypeSchema = z.object({
+ id: z.uuid(),
+ userId: z.uuid(),
+ name: z.string().min(1).max(255),
+ frontTemplate: z.string().min(1),
+ backTemplate: z.string().min(1),
+ isReversible: z.boolean(),
+ createdAt: z.coerce.date(),
+ updatedAt: z.coerce.date(),
+ deletedAt: z.coerce.date().nullable(),
+ syncVersion: z.number().int().min(0),
+});
+
+// NoteType creation input schema
+export const createNoteTypeSchema = z.object({
+ name: z.string().min(1).max(255),
+ frontTemplate: z.string().min(1),
+ backTemplate: z.string().min(1),
+ isReversible: z.boolean().default(false),
+});
+
+// NoteType update input schema
+export const updateNoteTypeSchema = z.object({
+ name: z.string().min(1).max(255).optional(),
+ frontTemplate: z.string().min(1).optional(),
+ backTemplate: z.string().min(1).optional(),
+ isReversible: z.boolean().optional(),
+});
+
+// NoteFieldType schema
+export const noteFieldTypeSchema = z.object({
+ id: z.uuid(),
+ noteTypeId: z.uuid(),
+ name: z.string().min(1).max(255),
+ order: z.number().int().min(0),
+ fieldType: fieldTypeSchema,
+ createdAt: z.coerce.date(),
+ updatedAt: z.coerce.date(),
+ deletedAt: z.coerce.date().nullable(),
+ syncVersion: z.number().int().min(0),
+});
+
+// NoteFieldType creation input schema
+export const createNoteFieldTypeSchema = z.object({
+ name: z.string().min(1).max(255),
+ order: z.number().int().min(0),
+ fieldType: fieldTypeSchema.default("text"),
+});
+
+// NoteFieldType update input schema
+export const updateNoteFieldTypeSchema = z.object({
+ name: z.string().min(1).max(255).optional(),
+ order: z.number().int().min(0).optional(),
+});
+
+// Note schema
+export const noteSchema = z.object({
+ id: z.uuid(),
+ deckId: z.uuid(),
+ noteTypeId: z.uuid(),
+ createdAt: z.coerce.date(),
+ updatedAt: z.coerce.date(),
+ deletedAt: z.coerce.date().nullable(),
+ syncVersion: z.number().int().min(0),
+});
+
+// Note creation input schema (fields is a map of fieldTypeId -> value)
+export const createNoteSchema = z.object({
+ noteTypeId: z.uuid(),
+ fields: z.record(z.uuid(), z.string()),
+});
+
+// Note update input schema
+export const updateNoteSchema = z.object({
+ fields: z.record(z.uuid(), z.string()),
+});
+
+// NoteFieldValue schema
+export const noteFieldValueSchema = z.object({
+ id: z.uuid(),
+ noteId: z.uuid(),
+ noteFieldTypeId: z.uuid(),
+ value: z.string(),
+ createdAt: z.coerce.date(),
+ updatedAt: z.coerce.date(),
+ syncVersion: z.number().int().min(0),
+});
+
// ReviewLog schema
export const reviewLogSchema = z.object({
id: z.uuid(),
@@ -138,3 +230,18 @@ export type CreateCardSchema = z.infer<typeof createCardSchema>;
export type UpdateCardSchema = z.infer<typeof updateCardSchema>;
export type ReviewLogSchema = z.infer<typeof reviewLogSchema>;
export type SubmitReviewSchema = z.infer<typeof submitReviewSchema>;
+export type FieldTypeSchema = z.infer<typeof fieldTypeSchema>;
+export type NoteTypeSchema = z.infer<typeof noteTypeSchema>;
+export type CreateNoteTypeSchema = z.infer<typeof createNoteTypeSchema>;
+export type UpdateNoteTypeSchema = z.infer<typeof updateNoteTypeSchema>;
+export type NoteFieldTypeSchema = z.infer<typeof noteFieldTypeSchema>;
+export type CreateNoteFieldTypeSchema = z.infer<
+ typeof createNoteFieldTypeSchema
+>;
+export type UpdateNoteFieldTypeSchema = z.infer<
+ typeof updateNoteFieldTypeSchema
+>;
+export type NoteSchema = z.infer<typeof noteSchema>;
+export type CreateNoteSchema = z.infer<typeof createNoteSchema>;
+export type UpdateNoteSchema = z.infer<typeof updateNoteSchema>;
+export type NoteFieldValueSchema = z.infer<typeof noteFieldValueSchema>;
diff --git a/src/server/schemas/note.test.ts b/src/server/schemas/note.test.ts
new file mode 100644
index 0000000..0a9b84b
--- /dev/null
+++ b/src/server/schemas/note.test.ts
@@ -0,0 +1,427 @@
+import { describe, expect, it } from "vitest";
+import {
+ createNoteFieldTypeSchema,
+ createNoteSchema,
+ createNoteTypeSchema,
+ noteFieldTypeSchema,
+ noteFieldValueSchema,
+ noteSchema,
+ noteTypeSchema,
+ updateNoteFieldTypeSchema,
+ updateNoteSchema,
+ updateNoteTypeSchema,
+} from "./index";
+
+describe("NoteType schemas", () => {
+ describe("noteTypeSchema", () => {
+ const validNoteType = {
+ id: "550e8400-e29b-41d4-a716-446655440000",
+ userId: "550e8400-e29b-41d4-a716-446655440001",
+ name: "Basic",
+ frontTemplate: "{{Front}}",
+ backTemplate: "{{Back}}",
+ isReversible: false,
+ createdAt: new Date("2024-01-01"),
+ updatedAt: new Date("2024-01-01"),
+ deletedAt: null,
+ syncVersion: 0,
+ };
+
+ it("should parse valid note type", () => {
+ const result = noteTypeSchema.safeParse(validNoteType);
+ expect(result.success).toBe(true);
+ });
+
+ it("should parse date strings", () => {
+ const result = noteTypeSchema.safeParse({
+ ...validNoteType,
+ createdAt: "2024-01-01T00:00:00.000Z",
+ updatedAt: "2024-01-01T00:00:00.000Z",
+ });
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.createdAt).toBeInstanceOf(Date);
+ expect(result.data.updatedAt).toBeInstanceOf(Date);
+ }
+ });
+
+ it("should reject invalid UUID for id", () => {
+ const result = noteTypeSchema.safeParse({
+ ...validNoteType,
+ id: "not-a-uuid",
+ });
+ expect(result.success).toBe(false);
+ });
+
+ it("should reject empty name", () => {
+ const result = noteTypeSchema.safeParse({
+ ...validNoteType,
+ name: "",
+ });
+ expect(result.success).toBe(false);
+ });
+
+ it("should reject name longer than 255 characters", () => {
+ const result = noteTypeSchema.safeParse({
+ ...validNoteType,
+ name: "a".repeat(256),
+ });
+ expect(result.success).toBe(false);
+ });
+
+ it("should reject empty frontTemplate", () => {
+ const result = noteTypeSchema.safeParse({
+ ...validNoteType,
+ frontTemplate: "",
+ });
+ expect(result.success).toBe(false);
+ });
+
+ it("should reject empty backTemplate", () => {
+ const result = noteTypeSchema.safeParse({
+ ...validNoteType,
+ backTemplate: "",
+ });
+ expect(result.success).toBe(false);
+ });
+
+ it("should reject negative syncVersion", () => {
+ const result = noteTypeSchema.safeParse({
+ ...validNoteType,
+ syncVersion: -1,
+ });
+ expect(result.success).toBe(false);
+ });
+
+ it("should accept deletedAt as date", () => {
+ const result = noteTypeSchema.safeParse({
+ ...validNoteType,
+ deletedAt: new Date("2024-01-02"),
+ });
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.deletedAt).toBeInstanceOf(Date);
+ }
+ });
+ });
+
+ describe("createNoteTypeSchema", () => {
+ it("should parse valid create input", () => {
+ const result = createNoteTypeSchema.safeParse({
+ name: "Basic",
+ frontTemplate: "{{Front}}",
+ backTemplate: "{{Back}}",
+ isReversible: true,
+ });
+ expect(result.success).toBe(true);
+ });
+
+ it("should default isReversible to false", () => {
+ const result = createNoteTypeSchema.safeParse({
+ name: "Basic",
+ frontTemplate: "{{Front}}",
+ backTemplate: "{{Back}}",
+ });
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.isReversible).toBe(false);
+ }
+ });
+
+ it("should reject missing name", () => {
+ const result = createNoteTypeSchema.safeParse({
+ frontTemplate: "{{Front}}",
+ backTemplate: "{{Back}}",
+ });
+ expect(result.success).toBe(false);
+ });
+
+ it("should reject missing frontTemplate", () => {
+ const result = createNoteTypeSchema.safeParse({
+ name: "Basic",
+ backTemplate: "{{Back}}",
+ });
+ expect(result.success).toBe(false);
+ });
+
+ it("should reject missing backTemplate", () => {
+ const result = createNoteTypeSchema.safeParse({
+ name: "Basic",
+ frontTemplate: "{{Front}}",
+ });
+ expect(result.success).toBe(false);
+ });
+ });
+
+ describe("updateNoteTypeSchema", () => {
+ it("should parse with all optional fields", () => {
+ const result = updateNoteTypeSchema.safeParse({});
+ expect(result.success).toBe(true);
+ });
+
+ it("should parse with partial fields", () => {
+ const result = updateNoteTypeSchema.safeParse({
+ name: "Updated Name",
+ });
+ expect(result.success).toBe(true);
+ });
+
+ it("should parse with all fields", () => {
+ const result = updateNoteTypeSchema.safeParse({
+ name: "Updated Name",
+ frontTemplate: "{{NewFront}}",
+ backTemplate: "{{NewBack}}",
+ isReversible: true,
+ });
+ expect(result.success).toBe(true);
+ });
+
+ it("should reject empty name if provided", () => {
+ const result = updateNoteTypeSchema.safeParse({
+ name: "",
+ });
+ expect(result.success).toBe(false);
+ });
+ });
+});
+
+describe("NoteFieldType schemas", () => {
+ describe("noteFieldTypeSchema", () => {
+ const validNoteFieldType = {
+ id: "550e8400-e29b-41d4-a716-446655440000",
+ noteTypeId: "550e8400-e29b-41d4-a716-446655440001",
+ name: "Front",
+ order: 0,
+ fieldType: "text" as const,
+ createdAt: new Date("2024-01-01"),
+ updatedAt: new Date("2024-01-01"),
+ deletedAt: null,
+ syncVersion: 0,
+ };
+
+ it("should parse valid note field type", () => {
+ const result = noteFieldTypeSchema.safeParse(validNoteFieldType);
+ expect(result.success).toBe(true);
+ });
+
+ it("should reject invalid fieldType", () => {
+ const result = noteFieldTypeSchema.safeParse({
+ ...validNoteFieldType,
+ fieldType: "invalid",
+ });
+ expect(result.success).toBe(false);
+ });
+
+ it("should reject negative order", () => {
+ const result = noteFieldTypeSchema.safeParse({
+ ...validNoteFieldType,
+ order: -1,
+ });
+ expect(result.success).toBe(false);
+ });
+
+ it("should reject empty name", () => {
+ const result = noteFieldTypeSchema.safeParse({
+ ...validNoteFieldType,
+ name: "",
+ });
+ expect(result.success).toBe(false);
+ });
+ });
+
+ describe("createNoteFieldTypeSchema", () => {
+ it("should parse valid create input", () => {
+ const result = createNoteFieldTypeSchema.safeParse({
+ name: "Front",
+ order: 0,
+ });
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.fieldType).toBe("text");
+ }
+ });
+
+ it("should accept explicit fieldType", () => {
+ const result = createNoteFieldTypeSchema.safeParse({
+ name: "Front",
+ order: 0,
+ fieldType: "text",
+ });
+ expect(result.success).toBe(true);
+ });
+
+ it("should reject missing name", () => {
+ const result = createNoteFieldTypeSchema.safeParse({
+ order: 0,
+ });
+ expect(result.success).toBe(false);
+ });
+
+ it("should reject missing order", () => {
+ const result = createNoteFieldTypeSchema.safeParse({
+ name: "Front",
+ });
+ expect(result.success).toBe(false);
+ });
+ });
+
+ describe("updateNoteFieldTypeSchema", () => {
+ it("should parse with all optional fields", () => {
+ const result = updateNoteFieldTypeSchema.safeParse({});
+ expect(result.success).toBe(true);
+ });
+
+ it("should parse with name only", () => {
+ const result = updateNoteFieldTypeSchema.safeParse({
+ name: "Updated Field",
+ });
+ expect(result.success).toBe(true);
+ });
+
+ it("should parse with order only", () => {
+ const result = updateNoteFieldTypeSchema.safeParse({
+ order: 1,
+ });
+ expect(result.success).toBe(true);
+ });
+ });
+});
+
+describe("Note schemas", () => {
+ describe("noteSchema", () => {
+ const validNote = {
+ id: "550e8400-e29b-41d4-a716-446655440000",
+ deckId: "550e8400-e29b-41d4-a716-446655440001",
+ noteTypeId: "550e8400-e29b-41d4-a716-446655440002",
+ createdAt: new Date("2024-01-01"),
+ updatedAt: new Date("2024-01-01"),
+ deletedAt: null,
+ syncVersion: 0,
+ };
+
+ it("should parse valid note", () => {
+ const result = noteSchema.safeParse(validNote);
+ expect(result.success).toBe(true);
+ });
+
+ it("should reject invalid deckId", () => {
+ const result = noteSchema.safeParse({
+ ...validNote,
+ deckId: "not-a-uuid",
+ });
+ expect(result.success).toBe(false);
+ });
+
+ it("should reject invalid noteTypeId", () => {
+ const result = noteSchema.safeParse({
+ ...validNote,
+ noteTypeId: "not-a-uuid",
+ });
+ expect(result.success).toBe(false);
+ });
+ });
+
+ describe("createNoteSchema", () => {
+ it("should parse valid create input", () => {
+ const result = createNoteSchema.safeParse({
+ noteTypeId: "550e8400-e29b-41d4-a716-446655440000",
+ fields: {
+ "550e8400-e29b-41d4-a716-446655440001": "Front content",
+ "550e8400-e29b-41d4-a716-446655440002": "Back content",
+ },
+ });
+ expect(result.success).toBe(true);
+ });
+
+ it("should accept empty fields", () => {
+ const result = createNoteSchema.safeParse({
+ noteTypeId: "550e8400-e29b-41d4-a716-446655440000",
+ fields: {},
+ });
+ expect(result.success).toBe(true);
+ });
+
+ it("should reject missing noteTypeId", () => {
+ const result = createNoteSchema.safeParse({
+ fields: {},
+ });
+ expect(result.success).toBe(false);
+ });
+
+ it("should reject missing fields", () => {
+ const result = createNoteSchema.safeParse({
+ noteTypeId: "550e8400-e29b-41d4-a716-446655440000",
+ });
+ expect(result.success).toBe(false);
+ });
+
+ it("should reject invalid UUID in fields key", () => {
+ const result = createNoteSchema.safeParse({
+ noteTypeId: "550e8400-e29b-41d4-a716-446655440000",
+ fields: {
+ "not-a-uuid": "content",
+ },
+ });
+ expect(result.success).toBe(false);
+ });
+ });
+
+ describe("updateNoteSchema", () => {
+ it("should parse valid update input", () => {
+ const result = updateNoteSchema.safeParse({
+ fields: {
+ "550e8400-e29b-41d4-a716-446655440001": "Updated content",
+ },
+ });
+ expect(result.success).toBe(true);
+ });
+
+ it("should reject missing fields", () => {
+ const result = updateNoteSchema.safeParse({});
+ expect(result.success).toBe(false);
+ });
+ });
+});
+
+describe("NoteFieldValue schemas", () => {
+ describe("noteFieldValueSchema", () => {
+ const validNoteFieldValue = {
+ id: "550e8400-e29b-41d4-a716-446655440000",
+ noteId: "550e8400-e29b-41d4-a716-446655440001",
+ noteFieldTypeId: "550e8400-e29b-41d4-a716-446655440002",
+ value: "Some content",
+ createdAt: new Date("2024-01-01"),
+ updatedAt: new Date("2024-01-01"),
+ syncVersion: 0,
+ };
+
+ it("should parse valid note field value", () => {
+ const result = noteFieldValueSchema.safeParse(validNoteFieldValue);
+ expect(result.success).toBe(true);
+ });
+
+ it("should accept empty value", () => {
+ const result = noteFieldValueSchema.safeParse({
+ ...validNoteFieldValue,
+ value: "",
+ });
+ expect(result.success).toBe(true);
+ });
+
+ it("should reject invalid noteId", () => {
+ const result = noteFieldValueSchema.safeParse({
+ ...validNoteFieldValue,
+ noteId: "not-a-uuid",
+ });
+ expect(result.success).toBe(false);
+ });
+
+ it("should reject invalid noteFieldTypeId", () => {
+ const result = noteFieldValueSchema.safeParse({
+ ...validNoteFieldValue,
+ noteFieldTypeId: "not-a-uuid",
+ });
+ expect(result.success).toBe(false);
+ });
+ });
+});