worldwide todo list: public todo list of 68 real, sourced problems (planet to street) at /projects/worldwide-todo. worldtodo schema (categories, tasks, change_requests), idempotent seed on deploy, public add/edit/delete requests with Turnstile + salted daily-rotating IP-hash rate limit, owner-only review queue (approve, edit & approve, reject) with live updates over /ws/worldtodo(-owner), JSON API, detail pages with OSM map and history; listed in projects, header, /start and sitemap
Some checks failed
Deploy production / deploy (push) Failing after 9s
Some checks failed
Deploy production / deploy (push) Failing after 9s
This commit is contained in:
@@ -30,4 +30,5 @@ export * from './stats';
|
||||
export * from './pubg-map';
|
||||
export * from './ai';
|
||||
export * from './movies';
|
||||
export * from './worldtodo';
|
||||
export * from './osu-goals';
|
||||
|
||||
@@ -27,3 +27,4 @@ export const dataholdersSchema = pgSchema('dataholders');
|
||||
export const statsSchema = pgSchema('stats');
|
||||
export const aiSchema = pgSchema('ai');
|
||||
export const moviesSchema = pgSchema('movies');
|
||||
export const worldtodoSchema = pgSchema('worldtodo');
|
||||
|
||||
124
src/lib/server/db/schema/worldtodo.ts
Normal file
124
src/lib/server/db/schema/worldtodo.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { sql } from 'drizzle-orm';
|
||||
import {
|
||||
bigint,
|
||||
check,
|
||||
customType,
|
||||
date,
|
||||
doublePrecision,
|
||||
index,
|
||||
jsonb,
|
||||
text,
|
||||
uniqueIndex
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { worldtodoSchema } from './schemas';
|
||||
import { timestamptz } from './helpers';
|
||||
|
||||
// The Worldwide Todo List (/projects/worldwide-todo): a public list of real problems phrased
|
||||
// as todos. Anyone can *request* an add, edit or delete (change_requests); nothing changes
|
||||
// until the owner approves it in the queue. No submitter data is stored beyond an optional
|
||||
// display name: no IP, email or user agent (rate limiting is in memory, see
|
||||
// #lib/server/worldtodo/limit). Seeded by scripts/seed-worldtodo.ts from data/worldtodo-seed.json.
|
||||
|
||||
// Kept in sync with #lib/worldtodo/shared (a unit test checks it); the schema folder is copied
|
||||
// into the app image on its own for the scripts, so it must not import from outside it.
|
||||
const WORLDTODO_SCALES = ['planet', 'region', 'country', 'city', 'street'] as const;
|
||||
const WORLDTODO_STATUSES = ['open', 'in_progress', 'done'] as const;
|
||||
const WORLDTODO_REQUEST_KINDS = ['add', 'edit', 'delete'] as const;
|
||||
const WORLDTODO_REQUEST_STATUSES = ['pending', 'approved', 'rejected'] as const;
|
||||
|
||||
const tsvector = customType<{ data: string }>({ dataType: () => 'tsvector' });
|
||||
|
||||
export const worldtodoScale = worldtodoSchema.enum('scale', WORLDTODO_SCALES);
|
||||
export const worldtodoStatus = worldtodoSchema.enum('status', WORLDTODO_STATUSES);
|
||||
export const worldtodoRequestKind = worldtodoSchema.enum('req_kind', WORLDTODO_REQUEST_KINDS);
|
||||
export const worldtodoRequestStatus = worldtodoSchema.enum(
|
||||
'req_status',
|
||||
WORLDTODO_REQUEST_STATUSES
|
||||
);
|
||||
|
||||
export const worldtodoCategories = worldtodoSchema.table('categories', {
|
||||
slug: text('slug').primaryKey(),
|
||||
label: text('label').notNull(),
|
||||
/** Icon key in #lib/worldtodo/icons (a lucide icon). */
|
||||
icon: text('icon').notNull(),
|
||||
/** Hex accent for the icon tile only. */
|
||||
colour: text('colour').notNull()
|
||||
});
|
||||
|
||||
export const worldtodoTasks = worldtodoSchema.table(
|
||||
'tasks',
|
||||
{
|
||||
id: bigint('id', { mode: 'number' }).primaryKey().generatedByDefaultAsIdentity(),
|
||||
slug: text('slug').notNull(),
|
||||
title: text('title').notNull(),
|
||||
description: text('description').notNull(),
|
||||
scale: worldtodoScale('scale').notNull(),
|
||||
category: text('category')
|
||||
.notNull()
|
||||
.references(() => worldtodoCategories.slug),
|
||||
locationName: text('location_name').notNull(),
|
||||
/** ISO 3166-1 alpha-2; null for planet-scale or multinational tasks. */
|
||||
countryCode: text('country_code'),
|
||||
lat: doublePrecision('lat'),
|
||||
lon: doublePrecision('lon'),
|
||||
status: worldtodoStatus('status').notNull().default('open'),
|
||||
sourceUrls: text('source_urls').array().notNull(),
|
||||
/** Date of the main source as given ('2026-09', '2026-10-21'). */
|
||||
sourceDate: text('source_date'),
|
||||
confidence: text('confidence').notNull().default('high'),
|
||||
/** Owner-facing caveat, also shown subtly on the detail page. */
|
||||
verificationNote: text('verification_note'),
|
||||
/** When the facts were last checked. */
|
||||
asOf: date('as_of').notNull(),
|
||||
createdAt: timestamptz('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamptz('updated_at').notNull().defaultNow(),
|
||||
/** Soft delete (approved delete requests); hard purge is a manual owner action. */
|
||||
deletedAt: timestamptz('deleted_at'),
|
||||
search: tsvector('search').generatedAlwaysAs(
|
||||
sql`to_tsvector('simple', title || ' ' || description || ' ' || location_name)`
|
||||
)
|
||||
},
|
||||
(t) => [
|
||||
uniqueIndex('tasks_slug_key').on(t.slug),
|
||||
index('tasks_search_idx').using('gin', t.search),
|
||||
index('tasks_scale_status_idx')
|
||||
.on(t.scale, t.status)
|
||||
.where(sql`deleted_at IS NULL`),
|
||||
check('tasks_title_check', sql`char_length(${t.title}) BETWEEN 5 AND 140`),
|
||||
check('tasks_description_check', sql`char_length(${t.description}) <= 2000`),
|
||||
check('tasks_lat_check', sql`${t.lat} BETWEEN -90 AND 90`),
|
||||
check('tasks_lon_check', sql`${t.lon} BETWEEN -180 AND 180`),
|
||||
check('tasks_sources_check', sql`cardinality(${t.sourceUrls}) >= 1`),
|
||||
check('tasks_confidence_check', sql`${t.confidence} IN ('high','medium')`)
|
||||
]
|
||||
);
|
||||
|
||||
export const worldtodoChangeRequests = worldtodoSchema.table(
|
||||
'change_requests',
|
||||
{
|
||||
id: bigint('id', { mode: 'number' }).primaryKey().generatedByDefaultAsIdentity(),
|
||||
kind: worldtodoRequestKind('kind').notNull(),
|
||||
/** Null for 'add'. */
|
||||
taskId: bigint('task_id', { mode: 'number' }).references(() => worldtodoTasks.id),
|
||||
/** add: full task fields; edit: only changed fields; delete: {}. Whitelisted keys only. */
|
||||
payload: jsonb('payload').$type<Record<string, unknown>>().notNull().default({}),
|
||||
/** What the owner actually applied (differs from payload after "edit & approve"). */
|
||||
applied: jsonb('applied').$type<Record<string, unknown>>(),
|
||||
/** The task's updated_at when the request was filed, to flag stale requests. */
|
||||
taskUpdatedAt: timestamptz('task_updated_at'),
|
||||
reason: text('reason').notNull(),
|
||||
/** Optional, the ONLY submitter info stored; shown nowhere except the owner queue. */
|
||||
displayName: text('display_name'),
|
||||
status: worldtodoRequestStatus('status').notNull().default('pending'),
|
||||
ownerNote: text('owner_note'),
|
||||
createdAt: timestamptz('created_at').notNull().defaultNow(),
|
||||
decidedAt: timestamptz('decided_at')
|
||||
},
|
||||
(t) => [
|
||||
index('change_requests_status_idx').on(t.status, t.createdAt),
|
||||
index('change_requests_task_idx').on(t.taskId, t.status),
|
||||
check('change_requests_reason_check', sql`char_length(${t.reason}) BETWEEN 10 AND 1000`),
|
||||
check('change_requests_display_name_check', sql`char_length(${t.displayName}) <= 40`),
|
||||
check('change_requests_task_check', sql`((${t.kind} = 'add') = (${t.taskId} IS NULL))`)
|
||||
]
|
||||
);
|
||||
Reference in New Issue
Block a user