feat: learning paths in studysets, synced, with progress and tags
Server: five synced tables, only additions (migration
20261002211346701-learning-paths):
- `learning_path`: its studyset, a fractional position, and its own
content in the payload {title, secondaryTitle, description}, the last
writer winning, as a studyset's;
- `learning_path_step`: its path, a position, and its kind ('info',
'singleChoice', 'multipleChoice', 'dragAndDrop', 'fillInTheBlank'),
fixed once created;
- `learning_path_step_revision`: a step's content as append-only
revisions, as a card's: {title, body, exercise, rightFeedback,
wrongFeedback, sources};
- `learning_path_answer`: an append-only log (right, wrong, continued,
reset), as reviews are, so answers from two devices never conflict;
- `learning_path_tag`: one row per assignment, as `card_tag`.
App, `feature/learning_path`: the five exercises and their JSON, grading
that marks only the learner's own wrong picks, progress worked out from
the log (a step is done once answered right since its last fresh start),
`LearningPathRepository` (steps saved on what they started from, merged
per field) and `PathProgressRepository` (answers, resets with Undo, the
times steps were done). Paths share the person's tags; deleting a path
or a step can be undone for a moment, and a studyset's deletion takes
its paths. Tests: models, codecs, repositories, tags, the device
migration guard, and sync tests against the server (four).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
43834ef42b
commit
91f6376b93
1 changed files with 34455 additions and 26 deletions
|
|
@ -0,0 +1,248 @@
|
|||
BEGIN;
|
||||
|
||||
--
|
||||
-- Function: gen_random_uuid_v7()
|
||||
-- Source: https://gist.github.com/kjmph/5bd772b2c2df145aa645b837da7eca74
|
||||
-- License: MIT (copyright notice included on the generator source code).
|
||||
--
|
||||
create or replace function gen_random_uuid_v7()
|
||||
returns uuid
|
||||
as $$
|
||||
begin
|
||||
-- use random v4 uuid as starting point (which has the same variant we need)
|
||||
-- then overlay timestamp
|
||||
-- then set version 7 by flipping the 2 and 1 bit in the version 4 string
|
||||
return encode(
|
||||
set_bit(
|
||||
set_bit(
|
||||
overlay(uuid_send(gen_random_uuid())
|
||||
placing substring(int8send(floor(extract(epoch from clock_timestamp()) * 1000)::bigint) from 3)
|
||||
from 1 for 6
|
||||
),
|
||||
52, 1
|
||||
),
|
||||
53, 1
|
||||
),
|
||||
'hex')::uuid;
|
||||
end
|
||||
$$
|
||||
language plpgsql
|
||||
volatile;
|
||||
|
||||
--
|
||||
-- ACTION CREATE TABLE
|
||||
--
|
||||
CREATE TABLE "learning_path" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(),
|
||||
"spaceId" bigint,
|
||||
"studysetId" uuid NOT NULL,
|
||||
"position" text NOT NULL,
|
||||
"keyId" text,
|
||||
"payload" bytea NOT NULL,
|
||||
"nonce" bytea
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX "learning_path_space_idx" ON "learning_path" USING btree ("spaceId");
|
||||
CREATE INDEX "learning_path_studyset_idx" ON "learning_path" USING btree ("studysetId", "position");
|
||||
|
||||
--
|
||||
-- ACTION CREATE TABLE
|
||||
--
|
||||
CREATE TABLE "learning_path_answer" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(),
|
||||
"spaceId" bigint,
|
||||
"stepId" uuid NOT NULL,
|
||||
"answeredAt" timestamp without time zone NOT NULL,
|
||||
"outcome" text NOT NULL,
|
||||
"answer" text
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX "learning_path_answer_space_idx" ON "learning_path_answer" USING btree ("spaceId");
|
||||
CREATE INDEX "learning_path_answer_step_idx" ON "learning_path_answer" USING btree ("stepId", "answeredAt");
|
||||
|
||||
--
|
||||
-- ACTION CREATE TABLE
|
||||
--
|
||||
CREATE TABLE "learning_path_step" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(),
|
||||
"spaceId" bigint,
|
||||
"pathId" uuid NOT NULL,
|
||||
"position" text NOT NULL,
|
||||
"kind" text NOT NULL
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX "learning_path_step_space_idx" ON "learning_path_step" USING btree ("spaceId");
|
||||
CREATE INDEX "learning_path_step_path_idx" ON "learning_path_step" USING btree ("pathId", "position");
|
||||
|
||||
--
|
||||
-- ACTION CREATE TABLE
|
||||
--
|
||||
CREATE TABLE "learning_path_step_revision" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(),
|
||||
"spaceId" bigint,
|
||||
"stepId" uuid NOT NULL,
|
||||
"parents" json NOT NULL,
|
||||
"authorId" uuid NOT NULL,
|
||||
"reportId" uuid,
|
||||
"keyId" text,
|
||||
"payload" bytea NOT NULL,
|
||||
"nonce" bytea
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX "learning_path_step_revision_space_idx" ON "learning_path_step_revision" USING btree ("spaceId");
|
||||
CREATE INDEX "learning_path_step_revision_step_idx" ON "learning_path_step_revision" USING btree ("stepId");
|
||||
|
||||
--
|
||||
-- ACTION CREATE TABLE
|
||||
--
|
||||
CREATE TABLE "learning_path_tag" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(),
|
||||
"spaceId" bigint,
|
||||
"pathId" uuid NOT NULL,
|
||||
"tagId" uuid NOT NULL
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX "learning_path_tag_space_idx" ON "learning_path_tag" USING btree ("spaceId");
|
||||
CREATE INDEX "learning_path_tag_path_idx" ON "learning_path_tag" USING btree ("pathId");
|
||||
CREATE INDEX "learning_path_tag_tag_idx" ON "learning_path_tag" USING btree ("tagId");
|
||||
|
||||
--
|
||||
-- ACTION CREATE FOREIGN KEY
|
||||
--
|
||||
ALTER TABLE ONLY "learning_path"
|
||||
ADD CONSTRAINT "learning_path_fk_0"
|
||||
FOREIGN KEY("spaceId")
|
||||
REFERENCES "offline_sync_spaces"("id")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE NO ACTION;
|
||||
ALTER TABLE ONLY "learning_path"
|
||||
ADD CONSTRAINT "learning_path_fk_1"
|
||||
FOREIGN KEY("studysetId")
|
||||
REFERENCES "studyset"("id")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE NO ACTION
|
||||
DEFERRABLE INITIALLY DEFERRED;
|
||||
|
||||
--
|
||||
-- ACTION CREATE FOREIGN KEY
|
||||
--
|
||||
ALTER TABLE ONLY "learning_path_answer"
|
||||
ADD CONSTRAINT "learning_path_answer_fk_0"
|
||||
FOREIGN KEY("spaceId")
|
||||
REFERENCES "offline_sync_spaces"("id")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE NO ACTION;
|
||||
ALTER TABLE ONLY "learning_path_answer"
|
||||
ADD CONSTRAINT "learning_path_answer_fk_1"
|
||||
FOREIGN KEY("stepId")
|
||||
REFERENCES "learning_path_step"("id")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE NO ACTION
|
||||
DEFERRABLE INITIALLY DEFERRED;
|
||||
|
||||
--
|
||||
-- ACTION CREATE FOREIGN KEY
|
||||
--
|
||||
ALTER TABLE ONLY "learning_path_step"
|
||||
ADD CONSTRAINT "learning_path_step_fk_0"
|
||||
FOREIGN KEY("spaceId")
|
||||
REFERENCES "offline_sync_spaces"("id")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE NO ACTION;
|
||||
ALTER TABLE ONLY "learning_path_step"
|
||||
ADD CONSTRAINT "learning_path_step_fk_1"
|
||||
FOREIGN KEY("pathId")
|
||||
REFERENCES "learning_path"("id")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE NO ACTION
|
||||
DEFERRABLE INITIALLY DEFERRED;
|
||||
|
||||
--
|
||||
-- ACTION CREATE FOREIGN KEY
|
||||
--
|
||||
ALTER TABLE ONLY "learning_path_step_revision"
|
||||
ADD CONSTRAINT "learning_path_step_revision_fk_0"
|
||||
FOREIGN KEY("spaceId")
|
||||
REFERENCES "offline_sync_spaces"("id")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE NO ACTION;
|
||||
ALTER TABLE ONLY "learning_path_step_revision"
|
||||
ADD CONSTRAINT "learning_path_step_revision_fk_1"
|
||||
FOREIGN KEY("stepId")
|
||||
REFERENCES "learning_path_step"("id")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE NO ACTION
|
||||
DEFERRABLE INITIALLY DEFERRED;
|
||||
|
||||
--
|
||||
-- ACTION CREATE FOREIGN KEY
|
||||
--
|
||||
ALTER TABLE ONLY "learning_path_tag"
|
||||
ADD CONSTRAINT "learning_path_tag_fk_0"
|
||||
FOREIGN KEY("spaceId")
|
||||
REFERENCES "offline_sync_spaces"("id")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE NO ACTION;
|
||||
ALTER TABLE ONLY "learning_path_tag"
|
||||
ADD CONSTRAINT "learning_path_tag_fk_1"
|
||||
FOREIGN KEY("pathId")
|
||||
REFERENCES "learning_path"("id")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE NO ACTION
|
||||
DEFERRABLE INITIALLY DEFERRED;
|
||||
ALTER TABLE ONLY "learning_path_tag"
|
||||
ADD CONSTRAINT "learning_path_tag_fk_2"
|
||||
FOREIGN KEY("tagId")
|
||||
REFERENCES "tag"("id")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE NO ACTION
|
||||
DEFERRABLE INITIALLY DEFERRED;
|
||||
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR halkyon_learn
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('halkyon_learn', '20261002211346701-learning-paths', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20261002211346701-learning-paths', "timestamp" = now();
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR serverpod
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('serverpod', '20260824182259319', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20260824182259319', "timestamp" = now();
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR serverpod_auth_idp
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('serverpod_auth_idp', '20260924105404509', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20260924105404509', "timestamp" = now();
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR serverpod_offline_sync
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('serverpod_offline_sync', '20260914143806119', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20260914143806119', "timestamp" = now();
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR serverpod_auth_core
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('serverpod_auth_core', '20260924105232991', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20260924105232991', "timestamp" = now();
|
||||
|
||||
|
||||
COMMIT;
|
||||
Loading…
Reference in a new issue