Some checks failed
Deploy production / deploy (push) Failing after 9s
Schema math (parts, chapters, sections, activity), seeded with all 100 chapters (Ch 0-94, appendix A-E), 647 sections, PDF pages, exercise counts and approximate prereqs. Triggers stamp started_at/completed_at and log status/exercise/note/section changes; math.apply_progress(jsonb) applies the JSON format bots send. Private by default via MATH_PRIVATE in #lib/math (owner-only prefix + lock in every tracker list). Overall + per-part progress, current chapter, prereq hints, quick status buttons, exercises stepper, notes, optional section ticks, activity log, dependency chart. Deploy preflight checks the math schema (4 tables, 100 chapters).
113 lines
4.0 KiB
TypeScript
113 lines
4.0 KiB
TypeScript
import { sql } from 'drizzle-orm';
|
|
import { check, index, integer, text, uniqueIndex } from 'drizzle-orm/pg-core';
|
|
import { mathSchema } from './schemas';
|
|
import { id, timestamps, timestamptz } from './helpers';
|
|
|
|
// Math tracker (/trackers/math): working through Evan Chen's "An Infinitely Large Napkin".
|
|
// Parts and chapters (with sections) are seeded from the book's outline in the migration.
|
|
// Progress lives on the chapter rows; triggers (in the migration) keep started_at/completed_at
|
|
// and log every status / exercise / note / section change to `activity`. Bots and scripts
|
|
// update progress with `select math.apply_progress('{"chapter":"3","status":"done"}')` or a
|
|
// plain UPDATE (both are logged). Prereqs are hints only, never locks.
|
|
|
|
// Statuses (kept in sync with MATH_STATUSES in #lib/math; the schema folder must not import
|
|
// from outside it): not_started, reading, exercises, done, skipped.
|
|
|
|
export const mathParts = mathSchema.table(
|
|
'parts',
|
|
{
|
|
id: id(),
|
|
/** "1".."22" or "appendix". */
|
|
key: text('key').notNull(),
|
|
/** Part number; null for the appendix. */
|
|
num: integer('num'),
|
|
title: text('title').notNull(),
|
|
sortOrder: integer('sort_order').notNull()
|
|
},
|
|
(t) => [uniqueIndex('parts_key_key').on(t.key)]
|
|
);
|
|
|
|
export const mathChapters = mathSchema.table(
|
|
'chapters',
|
|
{
|
|
id: id(),
|
|
/** Chapter number as text ("0".."94") or the appendix letter ("A".."E"). */
|
|
key: text('key').notNull(),
|
|
partId: integer('part_id')
|
|
.notNull()
|
|
.references(() => mathParts.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
|
title: text('title').notNull(),
|
|
/** PDF pages of the free Napkin.pdf (link with #page=N). */
|
|
pageStart: integer('page_start').notNull(),
|
|
pageEnd: integer('page_end').notNull(),
|
|
/** Problems at the end of the chapter; null when the chapter has none. */
|
|
exerciseCount: integer('exercise_count'),
|
|
/** Chapter keys this one builds on (approximate, from the book's dependency chart). */
|
|
prereqs: text('prereqs')
|
|
.array()
|
|
.notNull()
|
|
.default(sql`'{}'::text[]`),
|
|
status: text('status').notNull().default('not_started'),
|
|
exercisesDone: integer('exercises_done').notNull().default(0),
|
|
notes: text('notes'),
|
|
startedAt: timestamptz('started_at'),
|
|
completedAt: timestamptz('completed_at'),
|
|
sortOrder: integer('sort_order').notNull(),
|
|
...timestamps
|
|
},
|
|
(t) => [
|
|
uniqueIndex('chapters_key_key').on(t.key),
|
|
index('chapters_part_idx').on(t.partId, t.sortOrder),
|
|
check(
|
|
'chapters_status_check',
|
|
sql`${t.status} IN ('not_started','reading','exercises','done','skipped')`
|
|
),
|
|
check('chapters_exercises_done_check', sql`${t.exercisesDone} >= 0`)
|
|
]
|
|
);
|
|
|
|
export const mathSections = mathSchema.table(
|
|
'sections',
|
|
{
|
|
id: id(),
|
|
chapterId: integer('chapter_id')
|
|
.notNull()
|
|
.references(() => mathChapters.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
|
/** Section number, e.g. "3.2" (or "D.1"). */
|
|
key: text('key').notNull(),
|
|
title: text('title').notNull(),
|
|
sortOrder: integer('sort_order').notNull(),
|
|
/** Optional per-section tick. */
|
|
doneAt: timestamptz('done_at')
|
|
},
|
|
(t) => [uniqueIndex('sections_chapter_key_key').on(t.chapterId, t.key)]
|
|
);
|
|
|
|
export const mathActivity = mathSchema.table(
|
|
'activity',
|
|
{
|
|
id: id(),
|
|
chapterId: integer('chapter_id')
|
|
.notNull()
|
|
.references(() => mathChapters.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
|
sectionId: integer('section_id').references(() => mathSections.id, {
|
|
onDelete: 'set null',
|
|
onUpdate: 'cascade'
|
|
}),
|
|
/** status | exercises | note | section */
|
|
kind: text('kind').notNull(),
|
|
fromValue: text('from_value'),
|
|
toValue: text('to_value'),
|
|
/** exercises: done count change. */
|
|
delta: integer('delta'),
|
|
/** site | bot | sql (from the math.source setting, see math.apply_progress). */
|
|
source: text('source').notNull().default('sql'),
|
|
createdAt: timestamptz('created_at').notNull().defaultNow()
|
|
},
|
|
(t) => [
|
|
index('activity_created_idx').on(t.createdAt),
|
|
index('activity_chapter_idx').on(t.chapterId, t.createdAt),
|
|
check('activity_kind_check', sql`${t.kind} IN ('status','exercises','note','section')`)
|
|
]
|
|
);
|