diff --git a/README.md b/README.md index 36653594..5fd421ae 100644 --- a/README.md +++ b/README.md @@ -277,6 +277,37 @@ cleanly when a deployment lacks the required operation. See the organizer workfl The discovery location and `$skill-name` invocation follow the [official Codex skills documentation](https://developers.openai.com/codex/build-skills#where-codex-loads-local-skills). +### Excel / CSV agenda sync + +An event API key can preview and atomically apply a whole spreadsheet change set. Create and update +rows are published immediately; delete rows leave a cancelled record under the same `client_id` so +speaker calendars receive a real cancellation and replaying the same file is a no-op. Room, track, +and format accept either an event-scoped id or the display name an organizer sees in Excel. + +The Roman seed gives every scheduled debate a stable client id. Its deterministic +[`first-settlement-session-sync.csv`](docs/examples/first-settlement-session-sync.csv) fixture makes +exactly one create, one update, and one delete: + +```bash +export CICERO_API_KEY='' + +curl --fail-with-body \ + -H "Authorization: Bearer $CICERO_API_KEY" \ + -H 'Content-Type: text/csv' \ + --data-binary @docs/examples/first-settlement-session-sync.csv \ + 'http://localhost:3000/api/v1/events/first-settlement/sessions/sync?dryRun=true' + +curl --fail-with-body \ + -H "Authorization: Bearer $CICERO_API_KEY" \ + -H 'Content-Type: text/csv' \ + --data-binary @docs/examples/first-settlement-session-sync.csv \ + 'http://localhost:3000/api/v1/events/first-settlement/sessions/sync?dryRun=false' +``` + +The preview and first apply report `created: 1`, `updated: 1`, and `deleted: 1`. A second apply +reports `unchanged: 3` with no writes or calendar notifications. The same endpoint also accepts +`application/json` as `{ "rows": [{ "action": "create", "client_id": "...", ... }] }`. + ## Deployment **Vercel** is where the demo above runs. It is a stock Next build — no adapter, no config beyond diff --git a/app/api/v1/_lib/schemas.ts b/app/api/v1/_lib/schemas.ts index dca0c48f..a7eeee9d 100644 --- a/app/api/v1/_lib/schemas.ts +++ b/app/api/v1/_lib/schemas.ts @@ -177,6 +177,13 @@ export const sessionListQuery = z }) .strict(); +export const sessionSyncQuery = z.object({ + dryRun: z + .enum(['true', 'false']) + .optional() + .describe('Defaults to true. Set false only after reviewing the preview.'), +}); + export const speakerListQuery = z .object({ q: queryFilter.optional().describe('Search name, biography, company, role, links, and sessions'), diff --git a/app/api/v1/events/[slug]/sessions/sync/route.ts b/app/api/v1/events/[slug]/sessions/sync/route.ts new file mode 100644 index 00000000..e98ff5ff --- /dev/null +++ b/app/api/v1/events/[slug]/sessions/sync/route.ts @@ -0,0 +1,34 @@ +import { invalid } from '@/lib/errors'; +import { + parseSessionSyncCsv, + sessionSyncJsonBodySchema, + syncPublishedSessions, +} from '@/lib/services/session-sync'; +import { requireApiKey } from '../../../../_lib/auth'; +import { handle, json, parseBody, parseQuery, PRIVATE_CACHE } from '../../../../_lib/respond'; +import { sessionSyncQuery } from '../../../../_lib/schemas'; + +export const dynamic = 'force-dynamic'; + +const CSV_CONTENT_TYPES = new Set(['text/csv', 'application/csv', 'application/vnd.ms-excel']); + +export async function POST(request: Request, context: { params: Promise<{ slug: string }> }) { + return handle(async () => { + const { slug } = await context.params; + const key = await requireApiKey(request, slug); + const query = parseQuery(sessionSyncQuery, new URL(request.url)); + const contentType = request.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase(); + + const rows = CSV_CONTENT_TYPES.has(contentType ?? '') + ? parseSessionSyncCsv(await request.text()) + : contentType === 'application/json' + ? (await parseBody(sessionSyncJsonBodySchema, request)).rows + : undefined; + if (!rows) throw invalid('Send the agenda as text/csv or application/json'); + + const result = await syncPublishedSessions(key.eventId, rows, { + dryRun: query.dryRun !== 'false', + }); + return json(result, { headers: PRIVATE_CACHE }); + }); +} diff --git a/app/api/v1/openapi.json/route.ts b/app/api/v1/openapi.json/route.ts index 27cfe7b3..3c354178 100644 --- a/app/api/v1/openapi.json/route.ts +++ b/app/api/v1/openapi.json/route.ts @@ -1,4 +1,9 @@ import { appUrl } from '@/lib/env'; +import { + sessionSyncJsonBodySchema, + sessionSyncResultSchema, + sessionSyncRowSchema, +} from '@/lib/services/session-sync'; import { toJsonSchema, toParameters, type JsonSchema } from '../_lib/openapi'; import { PUBLIC_CACHE, handle, json } from '../_lib/respond'; import { @@ -17,6 +22,7 @@ import { publicFormSchema, sessionListQuery, sessionSchema, + sessionSyncQuery, speakerListQuery, speakerProfileSchema, sponsorListQuery, @@ -182,6 +188,9 @@ export function buildSpec(origin = appUrl()): JsonSchema { NewSubmissionResult: toJsonSchema(createSubmissionResponse), ProgramReconcileRequest: toJsonSchema(programReconcileBody), ProgramReconcileResult: toJsonSchema(programReconcileResponse), + SessionSyncRow: toJsonSchema(sessionSyncRowSchema), + SessionSyncRequest: toJsonSchema(sessionSyncJsonBodySchema), + SessionSyncResult: toJsonSchema(sessionSyncResultSchema), Error: toJsonSchema(errorResponse), }, }, @@ -212,6 +221,34 @@ export function buildSpec(origin = appUrl()): JsonSchema { }, }, }, + '/events/{slug}/sessions/sync': { + post: { + tags: ['Program'], + summary: 'Preview or apply an agenda collection sync', + description: + 'Requires an API key issued for this event. CSV rows use action and client_id columns. Create and update rows are published; delete rows are retained as cancelled so calendar cancellations and idempotent replay remain possible.', + operationId: 'syncSessions', + security: [{ bearerAuth: [] }], + parameters: [slugParam, ...toParameters(sessionSyncQuery, 'query')], + requestBody: { + required: true, + content: { + 'text/csv': { + schema: { + type: 'string', + description: + 'Excel-compatible CSV with action, client_id, title, room, starts_at and ends_at columns.', + }, + }, + 'application/json': { schema: ref('SessionSyncRequest') }, + }, + }, + responses: { + '200': okResponse('The preview or applied sync result', ref('SessionSyncResult')), + ...errors([401, 409, 422]), + }, + }, + }, '/events/{slug}/speakers': { get: { tags: ['Program'], diff --git a/db/migrations/0021_worried_salo.sql b/db/migrations/0021_worried_salo.sql new file mode 100644 index 00000000..3baf41f9 --- /dev/null +++ b/db/migrations/0021_worried_salo.sql @@ -0,0 +1 @@ +ALTER TABLE "scheduled_session" ADD CONSTRAINT "scheduled_session_event_client_id" UNIQUE("event_id","client_id"); \ No newline at end of file diff --git a/db/migrations/meta/0005_snapshot.json b/db/migrations/meta/0005_snapshot.json index a85e3f19..40e7193d 100644 --- a/db/migrations/meta/0005_snapshot.json +++ b/db/migrations/meta/0005_snapshot.json @@ -5617,4 +5617,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/db/migrations/meta/0021_snapshot.json b/db/migrations/meta/0021_snapshot.json new file mode 100644 index 00000000..20787a28 --- /dev/null +++ b/db/migrations/meta/0021_snapshot.json @@ -0,0 +1,7606 @@ +{ + "id": "922832ee-9873-4132-8904-68eaf85dd662", + "prevId": "03fdbdfa-5190-4478-a51b-47efd30ccf7a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accelevents_sync": { + "name": "accelevents_sync", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "remote_id": { + "name": "remote_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "sync_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_body": { + "name": "request_body", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "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()" + } + }, + "indexes": { + "accelevents_sync_event_idx": { + "name": "accelevents_sync_event_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accelevents_sync_event_id_event_id_fk": { + "name": "accelevents_sync_event_id_event_id_fk", + "tableFrom": "accelevents_sync", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "accelevents_sync_participant_id_participant_id_fk": { + "name": "accelevents_sync_participant_id_participant_id_fk", + "tableFrom": "accelevents_sync", + "tableTo": "participant", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_review": { + "name": "ai_review", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "submission_id": { + "name": "submission_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "review_round_id": { + "name": "review_round_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rationale_markdown": { + "name": "rationale_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "criterion_scores": { + "name": "criterion_scores", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ai_review_submission_idx": { + "name": "ai_review_submission_idx", + "columns": [ + { + "expression": "submission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_review_submission_id_submission_id_fk": { + "name": "ai_review_submission_id_submission_id_fk", + "tableFrom": "ai_review", + "tableTo": "submission", + "columnsFrom": [ + "submission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_review_review_round_id_review_round_id_fk": { + "name": "ai_review_review_round_id_review_round_id_fk", + "tableFrom": "ai_review", + "tableTo": "review_round", + "columnsFrom": [ + "review_round_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.airtable_sync": { + "name": "airtable_sync", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "remote_record_id": { + "name": "remote_record_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "sync_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "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()" + } + }, + "indexes": {}, + "foreignKeys": { + "airtable_sync_event_id_event_id_fk": { + "name": "airtable_sync_event_id_event_id_fk", + "tableFrom": "airtable_sync", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "airtable_sync_entity": { + "name": "airtable_sync_entity", + "nullsNotDistinct": false, + "columns": [ + "event_id", + "entity_type", + "entity_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "api_key_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'write'" + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "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()" + } + }, + "indexes": { + "api_key_prefix_idx": { + "name": "api_key_prefix_idx", + "columns": [ + { + "expression": "prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_event_id_event_id_fk": { + "name": "api_key_event_id_event_id_fk", + "tableFrom": "api_key", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contact": { + "name": "contact", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "company": { + "name": "company", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bio_markdown": { + "name": "bio_markdown", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headshot_url": { + "name": "headshot_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_fields": { + "name": "custom_fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "merged_into_contact_id": { + "name": "merged_into_contact_id", + "type": "uuid", + "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()" + } + }, + "indexes": { + "contact_owner_idx": { + "name": "contact_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contact_name_idx": { + "name": "contact_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contact_owner_user_id_user_id_fk": { + "name": "contact_owner_user_id_user_id_fk", + "tableFrom": "contact", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contact_owner_email": { + "name": "contact_owner_email", + "nullsNotDistinct": false, + "columns": [ + "owner_user_id", + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contact_activity": { + "name": "contact_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "prospect_id": { + "name": "prospect_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "contact_activity_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "contact_activity_contact_idx": { + "name": "contact_activity_contact_idx", + "columns": [ + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contact_activity_contact_id_contact_id_fk": { + "name": "contact_activity_contact_id_contact_id_fk", + "tableFrom": "contact_activity", + "tableTo": "contact", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "contact_activity_actor_user_id_user_id_fk": { + "name": "contact_activity_actor_user_id_user_id_fk", + "tableFrom": "contact_activity", + "tableTo": "user", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contact_campaign": { + "name": "contact_campaign", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_markdown": { + "name": "body_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipient_count": { + "name": "recipient_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "contact_campaign_owner_idx": { + "name": "contact_campaign_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contact_campaign_owner_user_id_user_id_fk": { + "name": "contact_campaign_owner_user_id_user_id_fk", + "tableFrom": "contact_campaign", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "contact_campaign_event_id_event_id_fk": { + "name": "contact_campaign_event_id_event_id_fk", + "tableFrom": "contact_campaign", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contact_campaign_recipient": { + "name": "contact_campaign_recipient", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "campaign_id": { + "name": "campaign_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rendered_subject": { + "name": "rendered_subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_log_id": { + "name": "email_log_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "contact_campaign_recipient_idx": { + "name": "contact_campaign_recipient_idx", + "columns": [ + { + "expression": "campaign_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contact_campaign_recipient_campaign_id_contact_campaign_id_fk": { + "name": "contact_campaign_recipient_campaign_id_contact_campaign_id_fk", + "tableFrom": "contact_campaign_recipient", + "tableTo": "contact_campaign", + "columnsFrom": [ + "campaign_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "contact_campaign_recipient_contact_id_contact_id_fk": { + "name": "contact_campaign_recipient_contact_id_contact_id_fk", + "tableFrom": "contact_campaign_recipient", + "tableTo": "contact", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "contact_campaign_recipient_email_log_id_email_log_id_fk": { + "name": "contact_campaign_recipient_email_log_id_email_log_id_fk", + "tableFrom": "contact_campaign_recipient", + "tableTo": "email_log", + "columnsFrom": [ + "email_log_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contact_event_link": { + "name": "contact_event_link", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "contact_event_link_contact_idx": { + "name": "contact_event_link_contact_idx", + "columns": [ + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contact_event_link_contact_id_contact_id_fk": { + "name": "contact_event_link_contact_id_contact_id_fk", + "tableFrom": "contact_event_link", + "tableTo": "contact", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "contact_event_link_event_id_event_id_fk": { + "name": "contact_event_link_event_id_event_id_fk", + "tableFrom": "contact_event_link", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "contact_event_link_participant_id_participant_id_fk": { + "name": "contact_event_link_participant_id_participant_id_fk", + "tableFrom": "contact_event_link", + "tableTo": "participant", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contact_event_link_pair": { + "name": "contact_event_link_pair", + "nullsNotDistinct": false, + "columns": [ + "contact_id", + "event_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contact_note": { + "name": "contact_note", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "prospect_id": { + "name": "prospect_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_markdown": { + "name": "body_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "contact_note_contact_idx": { + "name": "contact_note_contact_idx", + "columns": [ + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contact_note_contact_id_contact_id_fk": { + "name": "contact_note_contact_id_contact_id_fk", + "tableFrom": "contact_note", + "tableTo": "contact", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "contact_note_author_user_id_user_id_fk": { + "name": "contact_note_author_user_id_user_id_fk", + "tableFrom": "contact_note", + "tableTo": "user", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contact_segment": { + "name": "contact_segment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "segment_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'dynamic'" + }, + "filters": { + "name": "filters", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "member_contact_ids": { + "name": "member_contact_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "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": { + "contact_segment_owner_idx": { + "name": "contact_segment_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contact_segment_owner_user_id_user_id_fk": { + "name": "contact_segment_owner_user_id_user_id_fk", + "tableFrom": "contact_segment", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.content_revision": { + "name": "content_revision", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_kind": { + "name": "entity_kind", + "type": "content_revision_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "editor_user_id": { + "name": "editor_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "editor_name": { + "name": "editor_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "content_revision_entity_idx": { + "name": "content_revision_entity_idx", + "columns": [ + { + "expression": "entity_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "content_revision_event_id_event_id_fk": { + "name": "content_revision_event_id_event_id_fk", + "tableFrom": "content_revision", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "content_revision_editor_user_id_user_id_fk": { + "name": "content_revision_editor_user_id_user_id_fk", + "tableFrom": "content_revision", + "tableTo": "user", + "columnsFrom": [ + "editor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_field": { + "name": "crm_field", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "field_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "options": { + "name": "options", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "crm_field_owner_user_id_user_id_fk": { + "name": "crm_field_owner_user_id_user_id_fk", + "tableFrom": "crm_field", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "crm_field_owner_key": { + "name": "crm_field_owner_key", + "nullsNotDistinct": false, + "columns": [ + "owner_user_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_log": { + "name": "email_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "to_email": { + "name": "to_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_key": { + "name": "template_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ics_body": { + "name": "ics_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "email_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_message_id": { + "name": "provider_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "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()" + } + }, + "indexes": { + "email_log_event_created_idx": { + "name": "email_log_event_created_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_log_event_id_event_id_fk": { + "name": "email_log_event_id_event_id_fk", + "tableFrom": "email_log", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_template": { + "name": "email_template", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_markdown": { + "name": "body_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sms_body": { + "name": "sms_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "attach_ics": { + "name": "attach_ics", + "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()" + } + }, + "indexes": {}, + "foreignKeys": { + "email_template_event_id_event_id_fk": { + "name": "email_template_event_id_event_id_fk", + "tableFrom": "email_template", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_template_event_key": { + "name": "email_template_event_key", + "nullsNotDistinct": false, + "columns": [ + "event_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.event": { + "name": "event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tagline": { + "name": "tagline", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description_markdown": { + "name": "description_markdown", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'America/Los_Angeles'" + }, + "starts_at": { + "name": "starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "starts_on": { + "name": "starts_on", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ends_on": { + "name": "ends_on", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "website_url": { + "name": "website_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "venue_name": { + "name": "venue_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "venue_address": { + "name": "venue_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_file_id": { + "name": "logo_file_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "banner_file_id": { + "name": "banner_file_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "submission_seq": { + "name": "submission_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "session_seq": { + "name": "session_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "agenda_conflict_policy": { + "name": "agenda_conflict_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'warn'" + }, + "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": { + "event_owner_user_id_user_id_fk": { + "name": "event_owner_user_id_user_id_fk", + "tableFrom": "event", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "event_slug_unique": { + "name": "event_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": { + "event_agenda_conflict_policy_check": { + "name": "event_agenda_conflict_policy_check", + "value": "\"event\".\"agenda_conflict_policy\" in ('warn', 'block')" + } + }, + "isRLSEnabled": false + }, + "public.field_library_entry": { + "name": "field_library_entry", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "field_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "help_text": { + "name": "help_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "options": { + "name": "options", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "field_library_entry_event_id_event_id_fk": { + "name": "field_library_entry_event_id_event_id_fk", + "tableFrom": "field_library_entry", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "field_library_event_key": { + "name": "field_library_event_key", + "nullsNotDistinct": false, + "columns": [ + "event_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file": { + "name": "file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "uploaded_by_user_id": { + "name": "uploaded_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "root_file_id": { + "name": "root_file_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "file_event_idx": { + "name": "file_event_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "file_root_idx": { + "name": "file_root_idx", + "columns": [ + { + "expression": "root_file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "file_event_id_event_id_fk": { + "name": "file_event_id_event_id_fk", + "tableFrom": "file", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_uploaded_by_user_id_user_id_fk": { + "name": "file_uploaded_by_user_id_user_id_fk", + "tableFrom": "file", + "tableTo": "user", + "columnsFrom": [ + "uploaded_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_blob": { + "name": "file_blob", + "schema": "", + "columns": { + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "bytes": { + "name": "bytes", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_comment": { + "name": "file_comment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "file_id": { + "name": "file_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_markdown": { + "name": "body_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "file_comment_file_idx": { + "name": "file_comment_file_idx", + "columns": [ + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "file_comment_file_id_file_id_fk": { + "name": "file_comment_file_id_file_id_fk", + "tableFrom": "file_comment", + "tableTo": "file", + "columnsFrom": [ + "file_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_comment_author_user_id_user_id_fk": { + "name": "file_comment_author_user_id_user_id_fk", + "tableFrom": "file_comment", + "tableTo": "user", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_request": { + "name": "file_request", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "help_text": { + "name": "help_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accepted_types": { + "name": "accepted_types", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "max_size_mb": { + "name": "max_size_mb", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 25 + }, + "allow_multiple": { + "name": "allow_multiple", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "file_request_event_idx": { + "name": "file_request_event_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "file_request_event_id_event_id_fk": { + "name": "file_request_event_id_event_id_fk", + "tableFrom": "file_request", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form": { + "name": "form", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "form_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'cfp'" + }, + "target_type": { + "name": "target_type", + "type": "form_target_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'abstract'" + }, + "collects_participants": { + "name": "collects_participants", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_title": { + "name": "external_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "page_heading": { + "name": "page_heading", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "show_welcome": { + "name": "show_welcome", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "status": { + "name": "status", + "type": "form_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "intro_markdown": { + "name": "intro_markdown", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_participants": { + "name": "max_participants", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "opens_at": { + "name": "opens_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "closes_at": { + "name": "closes_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "max_submissions_per_user": { + "name": "max_submissions_per_user", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "allow_drafts": { + "name": "allow_drafts", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notify_emails": { + "name": "notify_emails", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "confirmation_subject": { + "name": "confirmation_subject", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmation_body_markdown": { + "name": "confirmation_body_markdown", + "type": "text", + "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()" + } + }, + "indexes": {}, + "foreignKeys": { + "form_event_id_event_id_fk": { + "name": "form_event_id_event_id_fk", + "tableFrom": "form", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "form_event_slug": { + "name": "form_event_slug", + "nullsNotDistinct": false, + "columns": [ + "event_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form_field": { + "name": "form_field", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "form_id": { + "name": "form_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "step": { + "name": "step", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "type": { + "name": "type", + "type": "field_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "builtin_key": { + "name": "builtin_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "help_text": { + "name": "help_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "placeholder": { + "name": "placeholder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "required": { + "name": "required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "entity": { + "name": "entity", + "type": "form_field_entity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'abstract'" + }, + "options": { + "name": "options", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "show_if": { + "name": "show_if", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "min_length": { + "name": "min_length", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "max_length": { + "name": "max_length", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "char_limit_group": { + "name": "char_limit_group", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "library_entry_id": { + "name": "library_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "form_field_form_idx": { + "name": "form_field_form_idx", + "columns": [ + { + "expression": "form_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_field_form_id_form_id_fk": { + "name": "form_field_form_id_form_id_fk", + "tableFrom": "form_field", + "tableTo": "form", + "columnsFrom": [ + "form_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "form_field_library_entry_id_field_library_entry_id_fk": { + "name": "form_field_library_entry_id_field_library_entry_id_fk", + "tableFrom": "form_field", + "tableTo": "field_library_entry", + "columnsFrom": [ + "library_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "form_field_form_key": { + "name": "form_field_form_key", + "nullsNotDistinct": false, + "columns": [ + "form_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form_participant_role": { + "name": "form_participant_role", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "form_id": { + "name": "form_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "participant_role_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "min_count": { + "name": "min_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_count": { + "name": "max_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "form_participant_role_form_idx": { + "name": "form_participant_role_form_idx", + "columns": [ + { + "expression": "form_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_participant_role_form_id_form_id_fk": { + "name": "form_participant_role_form_id_form_id_fk", + "tableFrom": "form_participant_role", + "tableTo": "form", + "columnsFrom": [ + "form_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "form_participant_role_form_kind": { + "name": "form_participant_role_form_kind", + "nullsNotDistinct": false, + "columns": [ + "form_id", + "kind" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inbound_rate_limit": { + "name": "inbound_rate_limit", + "schema": "", + "columns": { + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "window_started_at": { + "name": "window_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.magic_token": { + "name": "magic_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "redirect_to": { + "name": "redirect_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "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()" + } + }, + "indexes": { + "magic_token_user_idx": { + "name": "magic_token_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "magic_token_user_id_user_id_fk": { + "name": "magic_token_user_id_user_id_fk", + "tableFrom": "magic_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "magic_token_event_id_event_id_fk": { + "name": "magic_token_event_id_event_id_fk", + "tableFrom": "magic_token", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "magic_token_token_hash_unique": { + "name": "magic_token_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "token_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.membership": { + "name": "membership", + "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 + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "membership_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "membership_event_idx": { + "name": "membership_event_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "membership_user_id_user_id_fk": { + "name": "membership_user_id_user_id_fk", + "tableFrom": "membership", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "membership_event_id_event_id_fk": { + "name": "membership_event_id_event_id_fk", + "tableFrom": "membership", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "membership_user_event_role": { + "name": "membership_user_event_role", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "event_id", + "role" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_preference": { + "name": "notification_preference", + "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 + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_key": { + "name": "template_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'*'" + }, + "notify_email": { + "name": "notify_email", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "notify_sms": { + "name": "notify_sms", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quiet_start_minute": { + "name": "quiet_start_minute", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "quiet_end_minute": { + "name": "quiet_end_minute", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sms_hourly_limit": { + "name": "sms_hourly_limit", + "type": "integer", + "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()" + } + }, + "indexes": { + "notification_preference_event_idx": { + "name": "notification_preference_event_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notification_preference_user_id_user_id_fk": { + "name": "notification_preference_user_id_user_id_fk", + "tableFrom": "notification_preference", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_preference_event_id_event_id_fk": { + "name": "notification_preference_event_id_event_id_fk", + "tableFrom": "notification_preference", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "notification_preference_user_scope_template": { + "name": "notification_preference_user_scope_template", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "scope_key", + "template_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "notification_preference_scope_check": { + "name": "notification_preference_scope_check", + "value": "(\"notification_preference\".\"scope_key\" = 'global' and \"notification_preference\".\"event_id\" is null) or \"notification_preference\".\"scope_key\" = \"notification_preference\".\"event_id\"::text" + }, + "notification_preference_quiet_start_check": { + "name": "notification_preference_quiet_start_check", + "value": "\"notification_preference\".\"quiet_start_minute\" is null or (\"notification_preference\".\"quiet_start_minute\" between 0 and 1439)" + }, + "notification_preference_quiet_end_check": { + "name": "notification_preference_quiet_end_check", + "value": "\"notification_preference\".\"quiet_end_minute\" is null or (\"notification_preference\".\"quiet_end_minute\" between 0 and 1439)" + }, + "notification_preference_quiet_window_check": { + "name": "notification_preference_quiet_window_check", + "value": "(\"notification_preference\".\"quiet_start_minute\" is null) = (\"notification_preference\".\"quiet_end_minute\" is null)" + }, + "notification_preference_sms_rate_check": { + "name": "notification_preference_sms_rate_check", + "value": "\"notification_preference\".\"sms_hourly_limit\" is null or (\"notification_preference\".\"sms_hourly_limit\" between 1 and 100)" + } + }, + "isRLSEnabled": false + }, + "public.participant": { + "name": "participant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "salutation": { + "name": "salutation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "honorific": { + "name": "honorific", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pronouns": { + "name": "pronouns", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gender": { + "name": "gender", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "company": { + "name": "company", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bio_markdown": { + "name": "bio_markdown", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headshot_file_id": { + "name": "headshot_file_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "links": { + "name": "links", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_status": { + "name": "workflow_status", + "type": "speaker_workflow_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'invited'" + }, + "dietary_notes": { + "name": "dietary_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessibility_notes": { + "name": "accessibility_notes", + "type": "text", + "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()" + } + }, + "indexes": {}, + "foreignKeys": { + "participant_event_id_event_id_fk": { + "name": "participant_event_id_event_id_fk", + "tableFrom": "participant", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "participant_user_id_user_id_fk": { + "name": "participant_user_id_user_id_fk", + "tableFrom": "participant", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "participant_event_user": { + "name": "participant_event_user", + "nullsNotDistinct": false, + "columns": [ + "event_id", + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_role": { + "name": "participant_role", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "submission_id": { + "name": "submission_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "participant_role_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'speaker'" + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "participant_role_participant_idx": { + "name": "participant_role_participant_idx", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "participant_role_submission_id_submission_id_fk": { + "name": "participant_role_submission_id_submission_id_fk", + "tableFrom": "participant_role", + "tableTo": "submission", + "columnsFrom": [ + "submission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "participant_role_participant_id_participant_id_fk": { + "name": "participant_role_participant_id_participant_id_fk", + "tableFrom": "participant_role", + "tableTo": "participant", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "participant_role_pair": { + "name": "participant_role_pair", + "nullsNotDistinct": false, + "columns": [ + "submission_id", + "participant_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.persona": { + "name": "persona", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "persona_event_idx": { + "name": "persona_event_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "persona_event_id_event_id_fk": { + "name": "persona_event_id_event_id_fk", + "tableFrom": "persona", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.phone_verification_challenge": { + "name": "phone_verification_challenge", + "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 + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivery_transport": { + "name": "delivery_transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "verified_at": { + "name": "verified_at", + "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()" + } + }, + "indexes": { + "phone_verification_user_created_idx": { + "name": "phone_verification_user_created_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "phone_verification_challenge_user_id_user_id_fk": { + "name": "phone_verification_challenge_user_id_user_id_fk", + "tableFrom": "phone_verification_challenge", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "phone_verification_transport_check": { + "name": "phone_verification_transport_check", + "value": "\"phone_verification_challenge\".\"delivery_transport\" in ('log', 'twilio')" + } + }, + "isRLSEnabled": false + }, + "public.portal_page": { + "name": "portal_page", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_markdown": { + "name": "body_markdown", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "allow_raw_html": { + "name": "allow_raw_html", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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": { + "portal_page_event_id_event_id_fk": { + "name": "portal_page_event_id_event_id_fk", + "tableFrom": "portal_page", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "portal_page_event_slug": { + "name": "portal_page_event_slug", + "nullsNotDistinct": false, + "columns": [ + "event_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.portal_theme": { + "name": "portal_theme", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "logo_file_id": { + "name": "logo_file_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "welcome_markdown": { + "name": "welcome_markdown", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "support_email": { + "name": "support_email", + "type": "text", + "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()" + } + }, + "indexes": {}, + "foreignKeys": { + "portal_theme_event_id_event_id_fk": { + "name": "portal_theme_event_id_event_id_fk", + "tableFrom": "portal_theme", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "portal_theme_event_id_unique": { + "name": "portal_theme_event_id_unique", + "nullsNotDistinct": false, + "columns": [ + "event_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.prospect": { + "name": "prospect", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "stage": { + "name": "stage", + "type": "prospect_stage", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'identified'" + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rationale": { + "name": "rationale", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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": { + "prospect_owner_stage_idx": { + "name": "prospect_owner_stage_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stage", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "prospect_owner_user_id_user_id_fk": { + "name": "prospect_owner_user_id_user_id_fk", + "tableFrom": "prospect", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "prospect_contact_id_contact_id_fk": { + "name": "prospect_contact_id_contact_id_fk", + "tableFrom": "prospect", + "tableTo": "contact", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "prospect_event_id_event_id_fk": { + "name": "prospect_event_id_event_id_fk", + "tableFrom": "prospect", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_assignment": { + "name": "review_assignment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_round_id": { + "name": "review_round_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "submission_id": { + "name": "submission_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "reviewer_user_id": { + "name": "reviewer_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "review_assignment_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "review_assignment_reviewer_idx": { + "name": "review_assignment_reviewer_idx", + "columns": [ + { + "expression": "reviewer_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_assignment_submission_idx": { + "name": "review_assignment_submission_idx", + "columns": [ + { + "expression": "submission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_assignment_review_round_id_review_round_id_fk": { + "name": "review_assignment_review_round_id_review_round_id_fk", + "tableFrom": "review_assignment", + "tableTo": "review_round", + "columnsFrom": [ + "review_round_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "review_assignment_submission_id_submission_id_fk": { + "name": "review_assignment_submission_id_submission_id_fk", + "tableFrom": "review_assignment", + "tableTo": "submission", + "columnsFrom": [ + "submission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "review_assignment_reviewer_user_id_user_id_fk": { + "name": "review_assignment_reviewer_user_id_user_id_fk", + "tableFrom": "review_assignment", + "tableTo": "user", + "columnsFrom": [ + "reviewer_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "review_assignment_triple": { + "name": "review_assignment_triple", + "nullsNotDistinct": false, + "columns": [ + "review_round_id", + "submission_id", + "reviewer_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_recusal": { + "name": "review_recusal", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "submission_id": { + "name": "submission_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "reviewer_user_id": { + "name": "reviewer_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "review_recusal_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "review_round_id": { + "name": "review_round_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recused_at": { + "name": "recused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "released_at": { + "name": "released_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "released_by_user_id": { + "name": "released_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "review_recusal_submission_idx": { + "name": "review_recusal_submission_idx", + "columns": [ + { + "expression": "submission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_recusal_reviewer_idx": { + "name": "review_recusal_reviewer_idx", + "columns": [ + { + "expression": "reviewer_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_recusal_submission_id_submission_id_fk": { + "name": "review_recusal_submission_id_submission_id_fk", + "tableFrom": "review_recusal", + "tableTo": "submission", + "columnsFrom": [ + "submission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "review_recusal_reviewer_user_id_user_id_fk": { + "name": "review_recusal_reviewer_user_id_user_id_fk", + "tableFrom": "review_recusal", + "tableTo": "user", + "columnsFrom": [ + "reviewer_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "review_recusal_review_round_id_review_round_id_fk": { + "name": "review_recusal_review_round_id_review_round_id_fk", + "tableFrom": "review_recusal", + "tableTo": "review_round", + "columnsFrom": [ + "review_round_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "review_recusal_released_by_user_id_user_id_fk": { + "name": "review_recusal_released_by_user_id_user_id_fk", + "tableFrom": "review_recusal", + "tableTo": "user", + "columnsFrom": [ + "released_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "review_recusal_pair": { + "name": "review_recusal_pair", + "nullsNotDistinct": false, + "columns": [ + "submission_id", + "reviewer_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_round": { + "name": "review_round", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "review_round_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "decision_queue_bar_tenths": { + "name": "decision_queue_bar_tenths", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 30 + }, + "blind_until_close": { + "name": "blind_until_close", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "anonymized": { + "name": "anonymized", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "opens_at": { + "name": "opens_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "closes_at": { + "name": "closes_at", + "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()" + } + }, + "indexes": { + "review_round_event_idx": { + "name": "review_round_event_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_round_event_id_event_id_fk": { + "name": "review_round_event_id_event_id_fk", + "tableFrom": "review_round", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "review_round_decision_queue_bar_range": { + "name": "review_round_decision_queue_bar_range", + "value": "\"review_round\".\"decision_queue_bar_tenths\" between 10 and 50" + } + }, + "isRLSEnabled": false + }, + "public.room": { + "name": "room", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "capacity": { + "name": "capacity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "floor": { + "name": "floor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "room_event_idx": { + "name": "room_event_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "room_event_id_event_id_fk": { + "name": "room_event_id_event_id_fk", + "tableFrom": "room", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "room_event_name": { + "name": "room_event_name", + "nullsNotDistinct": false, + "columns": [ + "event_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saved_view": { + "name": "saved_view", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "surface": { + "name": "surface", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filters": { + "name": "filters", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "saved_view_user_surface_idx": { + "name": "saved_view_user_surface_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "surface", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "saved_view_event_id_event_id_fk": { + "name": "saved_view_event_id_event_id_fk", + "tableFrom": "saved_view", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saved_view_user_id_user_id_fk": { + "name": "saved_view_user_id_user_id_fk", + "tableFrom": "saved_view", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduled_session": { + "name": "scheduled_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "submission_id": { + "name": "submission_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ref": { + "name": "ref", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description_markdown": { + "name": "description_markdown", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "track_id": { + "name": "track_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "format_id": { + "name": "format_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "starts_at": { + "name": "starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "scheduled_session_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "ceu_credits": { + "name": "ceu_credits", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ics_uid": { + "name": "ics_uid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ics_sequence": { + "name": "ics_sequence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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": { + "scheduled_session_event_start_idx": { + "name": "scheduled_session_event_start_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starts_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduled_session_room_idx": { + "name": "scheduled_session_room_idx", + "columns": [ + { + "expression": "room_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduled_session_event_id_event_id_fk": { + "name": "scheduled_session_event_id_event_id_fk", + "tableFrom": "scheduled_session", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduled_session_submission_id_submission_id_fk": { + "name": "scheduled_session_submission_id_submission_id_fk", + "tableFrom": "scheduled_session", + "tableTo": "submission", + "columnsFrom": [ + "submission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "scheduled_session_room_id_room_id_fk": { + "name": "scheduled_session_room_id_room_id_fk", + "tableFrom": "scheduled_session", + "tableTo": "room", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "scheduled_session_track_id_track_id_fk": { + "name": "scheduled_session_track_id_track_id_fk", + "tableFrom": "scheduled_session", + "tableTo": "track", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "scheduled_session_format_id_session_format_id_fk": { + "name": "scheduled_session_format_id_session_format_id_fk", + "tableFrom": "scheduled_session", + "tableTo": "session_format", + "columnsFrom": [ + "format_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduled_session_event_ref": { + "name": "scheduled_session_event_ref", + "nullsNotDistinct": false, + "columns": [ + "event_id", + "ref" + ] + }, + "scheduled_session_event_client_id": { + "name": "scheduled_session_event_client_id", + "nullsNotDistinct": false, + "columns": [ + "event_id", + "client_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.score": { + "name": "score", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_assignment_id": { + "name": "review_assignment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "criterion_id": { + "name": "criterion_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "score_review_assignment_id_review_assignment_id_fk": { + "name": "score_review_assignment_id_review_assignment_id_fk", + "tableFrom": "score", + "tableTo": "review_assignment", + "columnsFrom": [ + "review_assignment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "score_criterion_id_scorecard_criterion_id_fk": { + "name": "score_criterion_id_scorecard_criterion_id_fk", + "tableFrom": "score", + "tableTo": "scorecard_criterion", + "columnsFrom": [ + "criterion_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "score_assignment_criterion": { + "name": "score_assignment_criterion", + "nullsNotDistinct": false, + "columns": [ + "review_assignment_id", + "criterion_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scorecard_criterion": { + "name": "scorecard_criterion", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_round_id": { + "name": "review_round_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "weight": { + "name": "weight", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "max_score": { + "name": "max_score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "scorecard_criterion_round_idx": { + "name": "scorecard_criterion_round_idx", + "columns": [ + { + "expression": "review_round_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scorecard_criterion_review_round_id_review_round_id_fk": { + "name": "scorecard_criterion_review_round_id_review_round_id_fk", + "tableFrom": "scorecard_criterion", + "tableTo": "review_round", + "columnsFrom": [ + "review_round_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_cookie": { + "name": "session_cookie", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "impersonated_by_user_id": { + "name": "impersonated_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_cookie_user_idx": { + "name": "session_cookie_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_cookie_user_id_user_id_fk": { + "name": "session_cookie_user_id_user_id_fk", + "tableFrom": "session_cookie", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_cookie_impersonated_by_user_id_user_id_fk": { + "name": "session_cookie_impersonated_by_user_id_user_id_fk", + "tableFrom": "session_cookie", + "tableTo": "user", + "columnsFrom": [ + "impersonated_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_cookie_token_hash_unique": { + "name": "session_cookie_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "token_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_format": { + "name": "session_format", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "duration_minutes": { + "name": "duration_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 30 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_format_event_idx": { + "name": "session_format_event_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_format_event_id_event_id_fk": { + "name": "session_format_event_id_event_id_fk", + "tableFrom": "session_format", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_recording": { + "name": "session_recording", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "session_recording_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "external_url": { + "name": "external_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_at", + "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()" + } + }, + "indexes": { + "session_recording_event_idx": { + "name": "session_recording_event_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_recording_event_id_event_id_fk": { + "name": "session_recording_event_id_event_id_fk", + "tableFrom": "session_recording", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recording_session_id_scheduled_session_id_fk": { + "name": "session_recording_session_id_scheduled_session_id_fk", + "tableFrom": "session_recording", + "tableTo": "scheduled_session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recording_file_id_file_id_fk": { + "name": "session_recording_file_id_file_id_fk", + "tableFrom": "session_recording", + "tableTo": "file", + "columnsFrom": [ + "file_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_recording_session_unique": { + "name": "session_recording_session_unique", + "nullsNotDistinct": false, + "columns": [ + "session_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "session_recording_exactly_one_source": { + "name": "session_recording_exactly_one_source", + "value": "(\"session_recording\".\"source\" = 'upload' AND \"session_recording\".\"file_id\" IS NOT NULL AND \"session_recording\".\"external_url\" IS NULL) OR (\"session_recording\".\"source\" = 'external' AND \"session_recording\".\"file_id\" IS NULL AND \"session_recording\".\"external_url\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.sms_consent": { + "name": "sms_consent", + "schema": "", + "columns": { + "phone": { + "name": "phone", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "status": { + "name": "status", + "type": "sms_consent_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consented_at": { + "name": "consented_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "opted_out_at": { + "name": "opted_out_at", + "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()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sms_log": { + "name": "sms_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "to_phone": { + "name": "to_phone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_phone": { + "name": "from_phone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_key": { + "name": "template_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "sms_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_message_id": { + "name": "provider_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status_updated_at": { + "name": "status_updated_at", + "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()" + } + }, + "indexes": { + "sms_log_event_created_idx": { + "name": "sms_log_event_created_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sms_log_provider_message_idx": { + "name": "sms_log_provider_message_idx", + "columns": [ + { + "expression": "provider_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sms_log_event_id_event_id_fk": { + "name": "sms_log_event_id_event_id_fk", + "tableFrom": "sms_log", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sponsor": { + "name": "sponsor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "sponsor_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'sponsor'" + }, + "status": { + "name": "status", + "type": "sponsor_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website_url": { + "name": "website_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "booth_location": { + "name": "booth_location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_file_id": { + "name": "logo_file_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sponsor_event_idx": { + "name": "sponsor_event_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sponsor_event_id_event_id_fk": { + "name": "sponsor_event_id_event_id_fk", + "tableFrom": "sponsor", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sponsor_event_kind_name": { + "name": "sponsor_event_kind_name", + "nullsNotDistinct": false, + "columns": [ + "event_id", + "kind", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.submission": { + "name": "submission", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "form_id": { + "name": "form_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ref": { + "name": "ref", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "submitter_user_id": { + "name": "submitter_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description_markdown": { + "name": "description_markdown", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "format_id": { + "name": "format_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "track_id": { + "name": "track_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "persona_id": { + "name": "persona_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "submission_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "content_status": { + "name": "content_status", + "type": "content_approval_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'approved'" + }, + "staged_decision": { + "name": "staged_decision", + "type": "submission_stage", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "staged_at": { + "name": "staged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "staged_by_user_id": { + "name": "staged_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "answers": { + "name": "answers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "decision_note": { + "name": "decision_note", + "type": "text", + "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()" + } + }, + "indexes": { + "submission_event_status_idx": { + "name": "submission_event_status_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "submission_form_idx": { + "name": "submission_form_idx", + "columns": [ + { + "expression": "form_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "submission_event_id_event_id_fk": { + "name": "submission_event_id_event_id_fk", + "tableFrom": "submission", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "submission_form_id_form_id_fk": { + "name": "submission_form_id_form_id_fk", + "tableFrom": "submission", + "tableTo": "form", + "columnsFrom": [ + "form_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "submission_submitter_user_id_user_id_fk": { + "name": "submission_submitter_user_id_user_id_fk", + "tableFrom": "submission", + "tableTo": "user", + "columnsFrom": [ + "submitter_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "submission_format_id_session_format_id_fk": { + "name": "submission_format_id_session_format_id_fk", + "tableFrom": "submission", + "tableTo": "session_format", + "columnsFrom": [ + "format_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "submission_track_id_track_id_fk": { + "name": "submission_track_id_track_id_fk", + "tableFrom": "submission", + "tableTo": "track", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "submission_persona_id_persona_id_fk": { + "name": "submission_persona_id_persona_id_fk", + "tableFrom": "submission", + "tableTo": "persona", + "columnsFrom": [ + "persona_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "submission_staged_by_user_id_user_id_fk": { + "name": "submission_staged_by_user_id_user_id_fk", + "tableFrom": "submission", + "tableTo": "user", + "columnsFrom": [ + "staged_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "submission_event_ref": { + "name": "submission_event_ref", + "nullsNotDistinct": false, + "columns": [ + "event_id", + "ref" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.submission_tag": { + "name": "submission_tag", + "schema": "", + "columns": { + "submission_id": { + "name": "submission_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "submission_tag_submission_id_submission_id_fk": { + "name": "submission_tag_submission_id_submission_id_fk", + "tableFrom": "submission_tag", + "tableTo": "submission", + "columnsFrom": [ + "submission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "submission_tag_tag_id_tag_id_fk": { + "name": "submission_tag_tag_id_tag_id_fk", + "tableFrom": "submission_tag", + "tableTo": "tag", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "submission_tag_pair": { + "name": "submission_tag_pair", + "nullsNotDistinct": false, + "columns": [ + "submission_id", + "tag_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tag": { + "name": "tag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tag_event_id_event_id_fk": { + "name": "tag_event_id_event_id_fk", + "tableFrom": "tag", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tag_event_name": { + "name": "tag_event_name", + "nullsNotDistinct": false, + "columns": [ + "event_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task": { + "name": "task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description_markdown": { + "name": "description_markdown", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "task_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "audience": { + "name": "audience", + "type": "task_audience", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'accepted_participants'" + }, + "scope": { + "name": "scope", + "type": "task_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'contact'" + }, + "submission_id": { + "name": "submission_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "form_id": { + "name": "form_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "file_request_id": { + "name": "file_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "link_url": { + "name": "link_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "due_at": { + "name": "due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "required": { + "name": "required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reminder_days_before": { + "name": "reminder_days_before", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_event_idx": { + "name": "task_event_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "task_event_id_event_id_fk": { + "name": "task_event_id_event_id_fk", + "tableFrom": "task", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_submission_id_submission_id_fk": { + "name": "task_submission_id_submission_id_fk", + "tableFrom": "task", + "tableTo": "submission", + "columnsFrom": [ + "submission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_form_id_form_id_fk": { + "name": "task_form_id_form_id_fk", + "tableFrom": "task", + "tableTo": "form", + "columnsFrom": [ + "form_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "task_file_request_id_file_request_id_fk": { + "name": "task_file_request_id_file_request_id_fk", + "tableFrom": "task", + "tableTo": "file_request", + "columnsFrom": [ + "file_request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_assignment": { + "name": "task_assignment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'not_started'" + }, + "submission_id": { + "name": "submission_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "task_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'contact'" + }, + "file_id": { + "name": "file_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "answers": { + "name": "answers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_reminded_at": { + "name": "last_reminded_at", + "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()" + } + }, + "indexes": { + "task_assignment_contact_key": { + "name": "task_assignment_contact_key", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"task_assignment\".\"submission_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_assignment_session_key": { + "name": "task_assignment_session_key", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "submission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"task_assignment\".\"submission_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_assignment_group_key": { + "name": "task_assignment_group_key", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "submission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"task_assignment\".\"scope\" = 'group'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_assignment_participant_idx": { + "name": "task_assignment_participant_idx", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_assignment_submission_idx": { + "name": "task_assignment_submission_idx", + "columns": [ + { + "expression": "submission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_assignment_status_idx": { + "name": "task_assignment_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "task_assignment_task_id_task_id_fk": { + "name": "task_assignment_task_id_task_id_fk", + "tableFrom": "task_assignment", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_assignment_participant_id_participant_id_fk": { + "name": "task_assignment_participant_id_participant_id_fk", + "tableFrom": "task_assignment", + "tableTo": "participant", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_assignment_submission_id_submission_id_fk": { + "name": "task_assignment_submission_id_submission_id_fk", + "tableFrom": "task_assignment", + "tableTo": "submission", + "columnsFrom": [ + "submission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_assignment_file_id_file_id_fk": { + "name": "task_assignment_file_id_file_id_fk", + "tableFrom": "task_assignment", + "tableTo": "file", + "columnsFrom": [ + "file_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.track": { + "name": "track", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "track_event_idx": { + "name": "track_event_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "track_event_id_event_id_fk": { + "name": "track_event_id_event_id_fk", + "tableFrom": "track", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "track_event_name": { + "name": "track_event_name", + "nullsNotDistinct": false, + "columns": [ + "event_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.track_reviewer": { + "name": "track_reviewer", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "track_id": { + "name": "track_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "reviewer_user_id": { + "name": "reviewer_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "track_reviewer_track_idx": { + "name": "track_reviewer_track_idx", + "columns": [ + { + "expression": "track_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "track_reviewer_reviewer_idx": { + "name": "track_reviewer_reviewer_idx", + "columns": [ + { + "expression": "reviewer_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "track_reviewer_track_id_track_id_fk": { + "name": "track_reviewer_track_id_track_id_fk", + "tableFrom": "track_reviewer", + "tableTo": "track", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "track_reviewer_reviewer_user_id_user_id_fk": { + "name": "track_reviewer_reviewer_user_id_user_id_fk", + "tableFrom": "track_reviewer", + "tableTo": "user", + "columnsFrom": [ + "reviewer_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "track_reviewer_pair": { + "name": "track_reviewer_pair", + "nullsNotDistinct": false, + "columns": [ + "track_id", + "reviewer_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.unsubscribe_token": { + "name": "unsubscribe_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "template_key": { + "name": "template_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "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()" + } + }, + "indexes": { + "unsubscribe_token_hash_idx": { + "name": "unsubscribe_token_hash_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "unsubscribe_token_user_id_user_id_fk": { + "name": "unsubscribe_token_user_id_user_id_fk", + "tableFrom": "unsubscribe_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "unsubscribe_token_event_id_event_id_fk": { + "name": "unsubscribe_token_event_id_event_id_fk", + "tableFrom": "unsubscribe_token", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unsubscribe_token_token_hash_unique": { + "name": "unsubscribe_token_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "token_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone_verified_at": { + "name": "phone_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "phone_verification_transport": { + "name": "phone_verification_transport", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notify_email": { + "name": "notify_email", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notify_sms": { + "name": "notify_sms", + "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()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": { + "user_phone_e164_check": { + "name": "user_phone_e164_check", + "value": "\"user\".\"phone\" is null or \"user\".\"phone\" ~ '^\\+[1-9][0-9]{7,14}$'" + } + }, + "isRLSEnabled": false + }, + "public.webhook_delivery": { + "name": "webhook_delivery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "endpoint_id": { + "name": "endpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "webhook_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delivered_at": { + "name": "delivered_at", + "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()" + } + }, + "indexes": { + "webhook_delivery_event_created_idx": { + "name": "webhook_delivery_event_created_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_delivery_endpoint_created_idx": { + "name": "webhook_delivery_endpoint_created_idx", + "columns": [ + { + "expression": "endpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_delivery_event_id_event_id_fk": { + "name": "webhook_delivery_event_id_event_id_fk", + "tableFrom": "webhook_delivery", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_delivery_endpoint_id_webhook_endpoint_id_fk": { + "name": "webhook_delivery_endpoint_id_webhook_endpoint_id_fk", + "tableFrom": "webhook_delivery", + "tableTo": "webhook_endpoint", + "columnsFrom": [ + "endpoint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_endpoint": { + "name": "webhook_endpoint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signing_secret": { + "name": "signing_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_types": { + "name": "event_types", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": { + "webhook_endpoint_event_idx": { + "name": "webhook_endpoint_event_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_endpoint_event_id_event_id_fk": { + "name": "webhook_endpoint_event_id_event_id_fk", + "tableFrom": "webhook_endpoint", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "webhook_endpoint_event_url": { + "name": "webhook_endpoint_event_url", + "nullsNotDistinct": false, + "columns": [ + "event_id", + "url" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.api_key_scope": { + "name": "api_key_scope", + "schema": "public", + "values": [ + "read", + "write" + ] + }, + "public.contact_activity_kind": { + "name": "contact_activity_kind", + "schema": "public", + "values": [ + "created", + "imported", + "updated", + "stage_change", + "event_added", + "email_sent", + "merged" + ] + }, + "public.content_approval_status": { + "name": "content_approval_status", + "schema": "public", + "values": [ + "in_review", + "approved", + "changes_requested" + ] + }, + "public.content_revision_kind": { + "name": "content_revision_kind", + "schema": "public", + "values": [ + "session", + "participant" + ] + }, + "public.email_status": { + "name": "email_status", + "schema": "public", + "values": [ + "queued", + "sent", + "failed" + ] + }, + "public.field_type": { + "name": "field_type", + "schema": "public", + "values": [ + "short_text", + "long_text", + "markdown", + "select", + "multi_select", + "radio", + "checkbox", + "number", + "email", + "url", + "date", + "file", + "section_break" + ] + }, + "public.form_field_entity": { + "name": "form_field_entity", + "schema": "public", + "values": [ + "abstract", + "participant" + ] + }, + "public.form_kind": { + "name": "form_kind", + "schema": "public", + "values": [ + "cfp", + "portal" + ] + }, + "public.form_status": { + "name": "form_status", + "schema": "public", + "values": [ + "draft", + "open", + "closed" + ] + }, + "public.form_target_type": { + "name": "form_target_type", + "schema": "public", + "values": [ + "abstract", + "session" + ] + }, + "public.membership_role": { + "name": "membership_role", + "schema": "public", + "values": [ + "organizer", + "reviewer", + "speaker" + ] + }, + "public.participant_role_kind": { + "name": "participant_role_kind", + "schema": "public", + "values": [ + "speaker", + "co_speaker", + "moderator", + "panelist" + ] + }, + "public.prospect_stage": { + "name": "prospect_stage", + "schema": "public", + "values": [ + "researching", + "identified", + "contacted", + "interested", + "confirmed", + "declined" + ] + }, + "public.review_assignment_status": { + "name": "review_assignment_status", + "schema": "public", + "values": [ + "pending", + "completed", + "declined" + ] + }, + "public.review_recusal_status": { + "name": "review_recusal_status", + "schema": "public", + "values": [ + "active", + "released" + ] + }, + "public.review_round_status": { + "name": "review_round_status", + "schema": "public", + "values": [ + "draft", + "open", + "closed" + ] + }, + "public.scheduled_session_status": { + "name": "scheduled_session_status", + "schema": "public", + "values": [ + "draft", + "published", + "cancelled" + ] + }, + "public.segment_kind": { + "name": "segment_kind", + "schema": "public", + "values": [ + "dynamic", + "curated" + ] + }, + "public.session_recording_source": { + "name": "session_recording_source", + "schema": "public", + "values": [ + "upload", + "external" + ] + }, + "public.sms_consent_status": { + "name": "sms_consent_status", + "schema": "public", + "values": [ + "opted_in", + "opted_out" + ] + }, + "public.sms_status": { + "name": "sms_status", + "schema": "public", + "values": [ + "queued", + "sent", + "delivered", + "undelivered", + "failed" + ] + }, + "public.speaker_workflow_status": { + "name": "speaker_workflow_status", + "schema": "public", + "values": [ + "invited", + "confirmed", + "declined", + "withdrawn" + ] + }, + "public.sponsor_kind": { + "name": "sponsor_kind", + "schema": "public", + "values": [ + "sponsor", + "exhibitor" + ] + }, + "public.sponsor_status": { + "name": "sponsor_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.submission_stage": { + "name": "submission_stage", + "schema": "public", + "values": [ + "accept", + "decline", + "hold" + ] + }, + "public.submission_status": { + "name": "submission_status", + "schema": "public", + "values": [ + "draft", + "submitted", + "under_review", + "accepted", + "declined", + "waitlisted", + "withdrawn" + ] + }, + "public.sync_status": { + "name": "sync_status", + "schema": "public", + "values": [ + "pending", + "synced", + "failed" + ] + }, + "public.task_audience": { + "name": "task_audience", + "schema": "public", + "values": [ + "all_participants", + "accepted_participants", + "manual" + ] + }, + "public.task_kind": { + "name": "task_kind", + "schema": "public", + "values": [ + "form", + "file_upload", + "acknowledge", + "link" + ] + }, + "public.task_scope": { + "name": "task_scope", + "schema": "public", + "values": [ + "contact", + "group", + "submission" + ] + }, + "public.task_status": { + "name": "task_status", + "schema": "public", + "values": [ + "not_started", + "in_progress", + "completed", + "waived" + ] + }, + "public.webhook_delivery_status": { + "name": "webhook_delivery_status", + "schema": "public", + "values": [ + "queued", + "delivered", + "failed" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/_journal.json b/db/migrations/meta/_journal.json index 5119ce0c..a833e731 100644 --- a/db/migrations/meta/_journal.json +++ b/db/migrations/meta/_journal.json @@ -148,6 +148,13 @@ "when": 1786852524963, "tag": "0020_foamy_reaper", "breakpoints": true + }, + { + "idx": 21, + "version": "7", + "when": 1786899260804, + "tag": "0021_worried_salo", + "breakpoints": true } ] } \ No newline at end of file diff --git a/db/schema.ts b/db/schema.ts index d7183c6c..fa3b6404 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -1011,6 +1011,7 @@ export const scheduledSession = pgTable( }, (t) => ({ uniqueRef: unique('scheduled_session_event_ref').on(t.eventId, t.ref), + uniqueClientId: unique('scheduled_session_event_client_id').on(t.eventId, t.clientId), byEventStart: index('scheduled_session_event_start_idx').on(t.eventId, t.startsAt), byRoom: index('scheduled_session_room_idx').on(t.roomId), }), diff --git a/db/seeds/first-settlement.ts b/db/seeds/first-settlement.ts index 130fc40c..948d83ef 100644 --- a/db/seeds/first-settlement.ts +++ b/db/seeds/first-settlement.ts @@ -852,30 +852,35 @@ export async function seedFirstSettlement( const placements = [ { motion: accepted[0], + clientId: 'roman-republic-restoration', room: curia, startsAt: atRome(day1, 9, 0), minutes: 45, }, { motion: accepted[1], + clientId: 'roman-provincial-command', room: curia, startsAt: atRome(day1, 10, 30), minutes: 30, }, { motion: accepted[3], + clientId: 'roman-first-among-senators', room: portico, startsAt: atRome(day1, 14, 0), minutes: 60, }, { motion: accepted[4], + clientId: 'roman-public-peace', room: temple, startsAt: atRome(day2, 10, 0), minutes: 30, }, { motion: accepted[2], + clientId: 'roman-name-augustus', room: curia, startsAt: atRome(day4, 11, 0), minutes: 45, @@ -897,6 +902,7 @@ export async function seedFirstSettlement( startsAt: placement.startsAt, endsAt: new Date(placement.startsAt.getTime() + placement.minutes * 60_000), status: 'published' as const, + clientId: placement.clientId, icsUid: newIcsUid(), })), ) diff --git a/docs/examples/first-settlement-session-sync.csv b/docs/examples/first-settlement-session-sync.csv new file mode 100644 index 00000000..b5032656 --- /dev/null +++ b/docs/examples/first-settlement-session-sync.csv @@ -0,0 +1,4 @@ +action,client_id,title,description,room,track,format,starts_at,ends_at,ceu_credits +update,roman-republic-restoration,On Returning the Republic to Senate and People — Revised,A revised opening statement returning emergency powers to ordinary government.,Curia Julia,Constitution & Office,Oratio,2027-01-13T09:15:00+01:00,2027-01-13T10:00:00+01:00, +delete,roman-provincial-command,,,,,,,, +create,roman-censors-register,The Censors' Register,A practical new session on restoring the Senate roll.,Portico of Octavia,Constitution & Office,Consilium,2027-01-13T12:00:00+01:00,2027-01-13T13:00:00+01:00, diff --git a/docs/openapi.json b/docs/openapi.json index c03fbd75..7918cd90 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -3093,6 +3093,243 @@ "operations" ] }, + "SessionSyncRow": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "create", + "update", + "delete" + ] + }, + "client_id": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "room": { + "type": "string" + }, + "track": { + "type": "string" + }, + "format": { + "type": "string" + }, + "starts_at": { + "type": "string", + "format": "date-time" + }, + "ends_at": { + "type": "string", + "format": "date-time" + }, + "ceu_credits": { + "type": "string" + } + }, + "required": [ + "action", + "client_id" + ], + "description": "One Excel-shaped agenda sync row" + }, + "SessionSyncRequest": { + "type": "object", + "properties": { + "rows": { + "type": "array", + "items": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "create", + "update", + "delete" + ] + }, + "client_id": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "room": { + "type": "string" + }, + "track": { + "type": "string" + }, + "format": { + "type": "string" + }, + "starts_at": { + "type": "string", + "format": "date-time" + }, + "ends_at": { + "type": "string", + "format": "date-time" + }, + "ceu_credits": { + "type": "string" + } + }, + "required": [ + "action", + "client_id" + ], + "description": "One Excel-shaped agenda sync row" + } + } + }, + "required": [ + "rows" + ], + "description": "Excel-shaped rows using the same column names as the CSV import" + }, + "SessionSyncResult": { + "type": "object", + "properties": { + "dryRun": { + "type": "boolean" + }, + "created": { + "type": "integer", + "minimum": 0 + }, + "updated": { + "type": "integer", + "minimum": 0 + }, + "deleted": { + "type": "integer", + "minimum": 0 + }, + "unchanged": { + "type": "integer", + "minimum": 0 + }, + "changes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "row": { + "type": "integer", + "minimum": 0 + }, + "clientId": { + "type": "string" + }, + "action": { + "type": "string", + "enum": [ + "create", + "update", + "delete" + ] + }, + "outcome": { + "type": "string", + "enum": [ + "created", + "updated", + "deleted", + "unchanged" + ] + } + }, + "required": [ + "row", + "clientId", + "action", + "outcome" + ] + } + }, + "conflicts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "room", + "track", + "speaker" + ] + }, + "severity": { + "type": "string", + "enum": [ + "error", + "warning" + ] + }, + "sessionIds": {}, + "message": { + "type": "string" + } + }, + "required": [ + "kind", + "severity", + "sessionIds", + "message" + ] + } + }, + "calendarNotifications": { + "type": "object", + "properties": { + "planned": { + "type": "integer", + "minimum": 0 + }, + "attempted": { + "type": "integer", + "minimum": 0 + }, + "failed": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "planned", + "attempted", + "failed" + ] + } + }, + "required": [ + "dryRun", + "created", + "updated", + "deleted", + "unchanged", + "changes", + "conflicts", + "calendarNotifications" + ] + }, "Error": { "type": "object", "properties": { @@ -3388,6 +3625,104 @@ } } }, + "/events/{slug}/sessions/sync": { + "post": { + "tags": [ + "Program" + ], + "summary": "Preview or apply an agenda collection sync", + "description": "Requires an API key issued for this event. CSV rows use action and client_id columns. Create and update rows are published; delete rows are retained as cancelled so calendar cancellations and idempotent replay remain possible.", + "operationId": "syncSessions", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "description": "The event's URL slug", + "schema": { + "type": "string" + } + }, + { + "name": "dryRun", + "in": "query", + "required": false, + "description": "Defaults to true. Set false only after reviewing the preview.", + "schema": { + "type": "string", + "enum": [ + "true", + "false" + ], + "description": "Defaults to true. Set false only after reviewing the preview." + } + } + ], + "requestBody": { + "required": true, + "content": { + "text/csv": { + "schema": { + "type": "string", + "description": "Excel-compatible CSV with action, client_id, title, room, starts_at and ends_at columns." + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionSyncRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The preview or applied sync result", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionSyncResult" + } + } + } + }, + "401": { + "description": "Missing or invalid credential", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Conflicts with an existing record", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "The request failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, "/events/{slug}/speakers": { "get": { "tags": [ diff --git a/lib/services/session-sync.test.ts b/lib/services/session-sync.test.ts new file mode 100644 index 00000000..244b4223 --- /dev/null +++ b/lib/services/session-sync.test.ts @@ -0,0 +1,332 @@ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; +import { AppError, notFound } from '@/lib/errors'; +import { publicEntries } from '@/lib/services/schedule'; +import { + parseSessionSyncCsv, + planSessionSync, + sessionSyncJsonBodySchema, + syncPublishedSessions, + type SessionSyncRow, + type SessionSyncSession, + type SessionSyncSnapshot, + type SessionSyncStore, +} from './session-sync'; + +const START = new Date('2027-01-13T08:00:00.000Z'); + +function session( + overrides: Partial & Pick, +): SessionSyncSession { + const { id, clientId, ...optionalOverrides } = overrides; + return { + id, + eventId: 'event-a', + ref: 1, + title: 'Existing session', + submissionId: null, + descriptionMarkdown: null, + roomId: 'room-curia', + trackId: 'track-constitution', + formatId: 'format-oratio', + startsAt: START, + endsAt: new Date(START.getTime() + 45 * 60_000), + status: 'published', + ceuCredits: null, + clientId, + icsUid: `${id}@cicero.events`, + icsSequence: 0, + speakers: [], + ...optionalOverrides, + }; +} + +function snapshot(sessions: SessionSyncSession[] = []): SessionSyncSnapshot { + return { + sessions, + rooms: [ + { id: 'room-curia', name: 'Curia Julia' }, + { id: 'room-portico', name: 'Portico of Octavia' }, + ], + tracks: [{ id: 'track-constitution', name: 'Constitution & Office' }], + formats: [ + { id: 'format-oratio', name: 'Oratio' }, + { id: 'format-consilium', name: 'Consilium' }, + ], + }; +} + +function cloneSnapshot(value: SessionSyncSnapshot): SessionSyncSnapshot { + return { + ...value, + sessions: value.sessions.map((row) => ({ + ...row, + startsAt: row.startsAt ? new Date(row.startsAt) : null, + endsAt: row.endsAt ? new Date(row.endsAt) : null, + speakers: row.speakers.map((speaker) => ({ ...speaker })), + })), + rooms: value.rooms.map((row) => ({ ...row })), + tracks: value.tracks.map((row) => ({ ...row })), + formats: value.formats.map((row) => ({ ...row })), + }; +} + +type InsertValues = Parameters[2]; +type UpdatePatch = Parameters[2]; + +class FakeStore implements SessionSyncStore { + events: Map; + sequences: Map; + transactionCalls = 0; + lockedReads = 0; + failClientId: string | null; + + constructor( + events: Map, + sequences = new Map(), + failClientId: string | null = null, + ) { + this.events = new Map( + [...events].map(([eventId, value]) => [eventId, cloneSnapshot(value)]), + ); + this.sequences = new Map(sequences); + this.failClientId = failClientId; + } + + async transaction(work: (transaction: SessionSyncStore) => Promise): Promise { + this.transactionCalls += 1; + const working = new FakeStore(this.events, this.sequences, this.failClientId); + const result = await work(working); + this.events = working.events; + this.sequences = working.sequences; + this.lockedReads += working.lockedReads; + return result; + } + + async loadSnapshot(eventId: string, lock: boolean): Promise { + if (lock) this.lockedReads += 1; + const value = this.events.get(eventId); + if (!value) throw notFound('That event'); + return cloneSnapshot(value); + } + + async reserveSessionRefs(eventId: string, count: number): Promise { + const current = this.sequences.get(eventId) ?? 0; + const refs = Array.from({ length: count }, (_, index) => current + index + 1); + this.sequences.set(eventId, current + count); + return refs; + } + + async insertSession(eventId: string, ref: number, values: InsertValues): Promise { + if (values.clientId === this.failClientId) throw new Error('injected write failure'); + const value = this.events.get(eventId); + if (!value) throw notFound('That event'); + const id = `created-${values.clientId}`; + value.sessions.push( + session({ + id, + eventId, + ref, + submissionId: null, + ...values, + icsUid: `${id}@cicero.events`, + }), + ); + return id; + } + + async updateSession(eventId: string, sessionId: string, patch: UpdatePatch): Promise { + const value = this.events.get(eventId); + const index = value?.sessions.findIndex((row) => row.id === sessionId) ?? -1; + if (!value || index < 0) throw new Error('session missing'); + value.sessions[index] = { ...value.sessions[index], ...patch }; + } +} + +function rows(input: unknown[]): SessionSyncRow[] { + return sessionSyncJsonBodySchema.parse({ rows: input }).rows; +} + +const romanFixture = readFileSync( + new URL('../../docs/examples/first-settlement-session-sync.csv', import.meta.url), + 'utf8', +); + +describe('session sync input', () => { + it('parses the deterministic Roman fixture as exactly one create, update, and delete', () => { + const parsed = parseSessionSyncCsv(romanFixture); + expect(parsed.map((row) => row.action).sort()).toEqual(['create', 'delete', 'update']); + expect(parsed).toHaveLength(3); + }); + + it('validates required headers, publishing fields, and timezone-aware ranges', () => { + expect(() => parseSessionSyncCsv('title\nA session')).toThrowError(AppError); + expect(() => + parseSessionSyncCsv( + 'action,client_id,title,room,starts_at,ends_at\ncreate,x,Talk,Curia Julia,2027-01-13T09:00:00,2027-01-13T08:00:00Z', + ), + ).toThrowError(AppError); + }); +}); + +describe('planSessionSync', () => { + it('plans create, update, and logical delete while publishing only the resulting collection', () => { + const current = snapshot([ + session({ + id: 'opening', + clientId: 'roman-republic-restoration', + title: 'On Returning the Republic to Senate and People', + }), + session({ + id: 'provincial', + clientId: 'roman-provincial-command', + title: 'A Ten-Year Command for the Unsettled Provinces', + startsAt: new Date('2027-01-13T09:30:00.000Z'), + endsAt: new Date('2027-01-13T10:00:00.000Z'), + }), + ]); + + const plan = planSessionSync(parseSessionSyncCsv(romanFixture), current); + expect(plan).toMatchObject({ created: 1, updated: 1, deleted: 1, unchanged: 0 }); + expect(plan.conflicts).toEqual([]); + expect(publicEntries(plan.finalEntries).map((row) => row.clientId).sort()).toEqual([ + 'roman-censors-register', + 'roman-republic-restoration', + ]); + expect( + plan.finalEntries.find((row) => row.clientId === 'roman-provincial-command')?.status, + ).toBe('cancelled'); + }); + + it('rejects cross-event client IDs and conflicting create reuse before any write', () => { + const current = snapshot([ + session({ id: 'other', clientId: 'other-event-id', eventId: 'event-b' }), + session({ id: 'opening', clientId: 'same-client' }), + ]); + const update = rows([ + { + action: 'update', + client_id: 'other-event-id', + title: 'Attempted cross-event update', + room: 'Curia Julia', + starts_at: '2027-01-13T09:00:00+01:00', + ends_at: '2027-01-13T09:45:00+01:00', + }, + ]); + const create = rows([ + { + action: 'create', + client_id: 'same-client', + title: 'Different data', + room: 'Curia Julia', + starts_at: '2027-01-13T09:00:00+01:00', + ends_at: '2027-01-13T09:45:00+01:00', + }, + ]); + + expect(() => planSessionSync(update, snapshot([current.sessions[1]]))).toThrowError(AppError); + expect(() => planSessionSync(create, current)).toThrowError(AppError); + }); +}); + +describe('syncPublishedSessions', () => { + it('previews without a transaction, then applies atomically and replays as unchanged', async () => { + const eventA = snapshot([ + session({ + id: 'opening', + clientId: 'roman-republic-restoration', + title: 'On Returning the Republic to Senate and People', + }), + session({ + id: 'provincial', + clientId: 'roman-provincial-command', + startsAt: new Date('2027-01-13T09:30:00.000Z'), + endsAt: new Date('2027-01-13T10:00:00.000Z'), + }), + ]); + const eventB = snapshot([ + session({ id: 'isolated', eventId: 'event-b', clientId: 'event-b-session' }), + ]); + const store = new FakeStore( + new Map([ + ['event-a', eventA], + ['event-b', eventB], + ]), + new Map([['event-a', 2]]), + ); + const parsed = parseSessionSyncCsv(romanFixture); + const notifications: Array<{ sessionId: string; cancel: boolean }> = []; + const notify = async (sessionId: string, options: { cancel: boolean }) => { + notifications.push({ sessionId, cancel: options.cancel }); + }; + + const preview = await syncPublishedSessions('event-a', parsed, { + dryRun: true, + store, + notify, + }); + expect(preview).toMatchObject({ dryRun: true, created: 1, updated: 1, deleted: 1 }); + expect(preview.calendarNotifications).toEqual({ planned: 3, attempted: 0, failed: 0 }); + expect(store.transactionCalls).toBe(0); + expect(store.events.get('event-a')).toEqual(eventA); + + const applied = await syncPublishedSessions('event-a', parsed, { + dryRun: false, + store, + notify, + }); + expect(applied).toMatchObject({ dryRun: false, created: 1, updated: 1, deleted: 1 }); + expect(applied.calendarNotifications).toEqual({ planned: 3, attempted: 3, failed: 0 }); + expect(notifications).toEqual( + expect.arrayContaining([ + { sessionId: 'opening', cancel: false }, + { sessionId: 'provincial', cancel: true }, + { sessionId: 'created-roman-censors-register', cancel: false }, + ]), + ); + expect(store.events.get('event-b')).toEqual(eventB); + + notifications.length = 0; + const replay = await syncPublishedSessions('event-a', parsed, { + dryRun: false, + store, + notify, + }); + expect(replay).toMatchObject({ created: 0, updated: 0, deleted: 0, unchanged: 3 }); + expect(replay.calendarNotifications).toEqual({ planned: 0, attempted: 0, failed: 0 }); + expect(notifications).toEqual([]); + }); + + it('rolls the whole collection back when a later write fails', async () => { + const original = snapshot(); + const store = new FakeStore(new Map([['event-a', original]]), new Map(), 'second'); + const input = rows([ + { + action: 'create', + client_id: 'first', + title: 'First', + room: 'Curia Julia', + starts_at: '2027-01-13T09:00:00+01:00', + ends_at: '2027-01-13T09:45:00+01:00', + }, + { + action: 'create', + client_id: 'second', + title: 'Second', + room: 'Portico of Octavia', + starts_at: '2027-01-13T10:00:00+01:00', + ends_at: '2027-01-13T10:45:00+01:00', + }, + ]); + + await expect( + syncPublishedSessions('event-a', input, { + dryRun: false, + store, + notify: async () => undefined, + }), + ).rejects.toThrow('injected write failure'); + expect(store.events.get('event-a')).toEqual(original); + expect(store.sequences.get('event-a')).toBeUndefined(); + }); +}); diff --git a/lib/services/session-sync.ts b/lib/services/session-sync.ts new file mode 100644 index 00000000..f4c7ff28 --- /dev/null +++ b/lib/services/session-sync.ts @@ -0,0 +1,728 @@ +import { and, eq, inArray, sql } from 'drizzle-orm'; +import { z } from 'zod'; +import { getDb, type Database } from '@/db/client'; +import { + event, + participant, + participantRole, + room, + scheduledSession, + sessionFormat, + track, + user, +} from '@/db/schema'; +import { normalizeHeader, parseCsvTable } from '@/lib/csv'; +import { conflict, invalid, notFound } from '@/lib/errors'; +import { newIcsUid } from '@/lib/ics'; +import { sendSessionInvites } from '@/lib/services/comms'; +import { + canPublish, + detectConflicts, + isPlaced, + type Conflict, + type ScheduleEntry, + type SpeakerRef, +} from '@/lib/services/schedule'; + +const optionalCell = z.preprocess( + (value) => (typeof value === 'string' && value.trim() === '' ? undefined : value), + z.string().trim().optional(), +); +const optionalTimestamp = z.preprocess( + (value) => (typeof value === 'string' && value.trim() === '' ? undefined : value), + z.string().datetime({ offset: true }).optional(), +); + +export const sessionSyncRowSchema = z + .object({ + action: z.preprocess( + (value) => (typeof value === 'string' ? value.trim().toLowerCase() : value), + z.enum(['create', 'update', 'delete']), + ), + client_id: z.string().trim().min(1).max(120), + title: optionalCell, + description: optionalCell, + room: optionalCell, + track: optionalCell, + format: optionalCell, + starts_at: optionalTimestamp, + ends_at: optionalTimestamp, + ceu_credits: optionalCell, + }) + .superRefine((row, context) => { + if (row.action === 'delete') return; + + for (const field of ['title', 'room', 'starts_at', 'ends_at'] as const) { + if (!row[field]) { + context.addIssue({ + code: z.ZodIssueCode.custom, + path: [field], + message: `${field} is required for ${row.action}`, + }); + } + } + + if (row.starts_at && row.ends_at) { + const startsAt = new Date(row.starts_at); + const endsAt = new Date(row.ends_at); + if (endsAt.getTime() <= startsAt.getTime()) { + context.addIssue({ + code: z.ZodIssueCode.custom, + path: ['ends_at'], + message: 'ends_at must be after starts_at', + }); + } + } + }) + .describe('One Excel-shaped agenda sync row'); + +export const sessionSyncJsonBodySchema = z + .object({ rows: z.array(sessionSyncRowSchema).min(1).max(1000) }) + .describe('Excel-shaped rows using the same column names as the CSV import'); + +export const sessionSyncResultSchema = z.object({ + dryRun: z.boolean(), + created: z.number().int().nonnegative(), + updated: z.number().int().nonnegative(), + deleted: z.number().int().nonnegative(), + unchanged: z.number().int().nonnegative(), + changes: z.array( + z.object({ + row: z.number().int().positive(), + clientId: z.string(), + action: z.enum(['create', 'update', 'delete']), + outcome: z.enum(['created', 'updated', 'deleted', 'unchanged']), + }), + ), + conflicts: z.array( + z.object({ + kind: z.enum(['room', 'track', 'speaker']), + severity: z.enum(['error', 'warning']), + sessionIds: z.tuple([z.string(), z.string()]), + message: z.string(), + }), + ), + calendarNotifications: z.object({ + planned: z.number().int().nonnegative(), + attempted: z.number().int().nonnegative(), + failed: z.number().int().nonnegative(), + }), +}); + +export type SessionSyncRow = z.infer; +export type SessionSyncResult = z.infer; + +const HEADER_ALIASES: Record = { + action: 'action', + 'client id': 'client_id', + clientid: 'client_id', + title: 'title', + description: 'description', + 'description markdown': 'description', + room: 'room', + track: 'track', + format: 'format', + 'starts at': 'starts_at', + start: 'starts_at', + 'ends at': 'ends_at', + end: 'ends_at', + 'ceu credits': 'ceu_credits', +}; + +export function parseSessionSyncCsv(csv: string): SessionSyncRow[] { + const table = parseCsvTable(csv); + const columns = table.headers.map((header) => HEADER_ALIASES[normalizeHeader(header)]); + const canonical = columns.filter((column): column is keyof SessionSyncRow => Boolean(column)); + const missing = (['action', 'client_id'] as const).filter((column) => !canonical.includes(column)); + if (missing.length > 0) { + throw invalid('That session sync CSV is missing required columns', { + headers: `Add ${missing.join(', ')}`, + }); + } + if (new Set(canonical).size !== canonical.length) { + throw invalid('That session sync CSV maps more than one header to the same field'); + } + + const rows = table.rows.map((cells) => + Object.fromEntries( + columns.flatMap((column, index) => (column ? [[column, cells[index]]] : [])), + ), + ); + const parsed = sessionSyncJsonBodySchema.safeParse({ rows }); + if (!parsed.success) { + throw invalid('That session sync CSV is not valid', zodDetails(parsed.error)); + } + return parsed.data.rows; +} + +function zodDetails(error: z.ZodError): Record { + return Object.fromEntries( + error.issues.map((issue) => [issue.path.join('.') || '_', issue.message]), + ); +} + +export type SessionSyncSession = ScheduleEntry & { + eventId: string; + descriptionMarkdown: string | null; + icsUid: string; + icsSequence: number; +}; + +export type SessionSyncSnapshot = { + sessions: SessionSyncSession[]; + rooms: Array<{ id: string; name: string }>; + tracks: Array<{ id: string; name: string }>; + formats: Array<{ id: string; name: string }>; +}; + +type SessionValues = { + title: string; + descriptionMarkdown: string | null; + roomId: string; + trackId: string | null; + formatId: string | null; + startsAt: Date; + endsAt: Date; + ceuCredits: string | null; + clientId: string; + status: 'published'; +}; + +type CreateMutation = { + kind: 'create'; + row: SessionSyncRow; + rowNumber: number; + values: SessionValues; +}; + +type UpdateMutation = { + kind: 'update'; + row: SessionSyncRow; + rowNumber: number; + existing: SessionSyncSession; + values: SessionValues; +}; + +type DeleteMutation = { + kind: 'delete'; + row: SessionSyncRow; + rowNumber: number; + existing: SessionSyncSession; +}; + +type SessionMutation = CreateMutation | UpdateMutation | DeleteMutation; + +export type SessionSyncPlan = Omit & { + mutations: SessionMutation[]; + finalEntries: ScheduleEntry[]; + notificationsPlanned: number; +}; + +function resolveReference( + value: string | undefined, + choices: Array<{ id: string; name: string }>, + field: 'room' | 'track' | 'format', + rowNumber: number, +): string | null { + if (!value) return null; + const byId = choices.find((choice) => choice.id === value); + if (byId) return byId.id; + + const matches = choices.filter( + (choice) => choice.name.trim().toLocaleLowerCase() === value.trim().toLocaleLowerCase(), + ); + if (matches.length === 1) return matches[0].id; + if (matches.length > 1) { + throw invalid('That session sync row uses an ambiguous event value', { + [`rows.${rowNumber}.${field}`]: `${value} matches more than one ${field}`, + }); + } + throw invalid('That session sync row references a value outside this event', { + [`rows.${rowNumber}.${field}`]: `${value} is not an event ${field}`, + }); +} + +function valuesForRow( + row: SessionSyncRow, + rowNumber: number, + snapshot: SessionSyncSnapshot, +): SessionValues { + const values: SessionValues = { + title: row.title as string, + descriptionMarkdown: row.description ?? null, + roomId: resolveReference(row.room, snapshot.rooms, 'room', rowNumber) as string, + trackId: resolveReference(row.track, snapshot.tracks, 'track', rowNumber), + formatId: resolveReference(row.format, snapshot.formats, 'format', rowNumber), + startsAt: new Date(row.starts_at as string), + endsAt: new Date(row.ends_at as string), + ceuCredits: row.ceu_credits ?? null, + clientId: row.client_id, + status: 'published', + }; + + const entry: ScheduleEntry = { + id: `sync:${row.client_id}`, + ref: 0, + title: values.title, + submissionId: null, + roomId: values.roomId, + trackId: values.trackId, + formatId: values.formatId, + startsAt: values.startsAt, + endsAt: values.endsAt, + status: values.status, + ceuCredits: values.ceuCredits, + clientId: values.clientId, + speakers: [], + }; + if (!canPublish(entry)) { + throw invalid('Every created or updated row needs a room and a valid time range', { + [`rows.${rowNumber}`]: 'This row cannot be published', + }); + } + return values; +} + +function sameDate(left: Date | null, right: Date): boolean { + return left instanceof Date && left.getTime() === right.getTime(); +} + +function matchesValues(existing: SessionSyncSession, values: SessionValues): boolean { + return ( + existing.title === values.title && + existing.descriptionMarkdown === values.descriptionMarkdown && + existing.roomId === values.roomId && + existing.trackId === values.trackId && + existing.formatId === values.formatId && + sameDate(existing.startsAt, values.startsAt) && + sameDate(existing.endsAt, values.endsAt) && + existing.ceuCredits === values.ceuCredits && + existing.status === values.status + ); +} + +function withValues(existing: SessionSyncSession, values: SessionValues): SessionSyncSession { + return { ...existing, ...values }; +} + +function provisional(values: SessionValues): ScheduleEntry { + return { + id: `sync:${values.clientId}`, + ref: 0, + title: values.title, + submissionId: null, + roomId: values.roomId, + trackId: values.trackId, + formatId: values.formatId, + startsAt: values.startsAt, + endsAt: values.endsAt, + status: values.status, + ceuCredits: values.ceuCredits, + clientId: values.clientId, + speakers: [], + }; +} + +function publicConflict(conflictRow: Conflict): SessionSyncPlan['conflicts'][number] { + return { + kind: conflictRow.kind, + severity: conflictRow.severity, + sessionIds: conflictRow.sessionIds, + message: conflictRow.message, + }; +} + +export function planSessionSync( + rows: SessionSyncRow[], + snapshot: SessionSyncSnapshot, +): SessionSyncPlan { + const duplicateInput = rows.find( + (row, index) => rows.findIndex((candidate) => candidate.client_id === row.client_id) !== index, + ); + if (duplicateInput) { + throw invalid('Each client_id may appear only once per sync request', { + client_id: duplicateInput.client_id, + }); + } + + const existingByClient = new Map(); + for (const session of snapshot.sessions) { + if (!session.clientId) continue; + if (existingByClient.has(session.clientId)) { + throw conflict('Existing sessions have duplicate client IDs', { + client_id: session.clientId, + }); + } + existingByClient.set(session.clientId, session); + } + + const finalById = new Map(snapshot.sessions.map((session) => [session.id, session])); + const additions: ScheduleEntry[] = []; + const mutations: SessionMutation[] = []; + const changes: SessionSyncPlan['changes'] = []; + + rows.forEach((row, index) => { + const rowNumber = index + 1; + const existing = existingByClient.get(row.client_id); + + if (row.action === 'delete') { + if (!existing) { + throw conflict('A delete row did not match a session in this event', { + [`rows.${rowNumber}.client_id`]: row.client_id, + }); + } + if (existing.status === 'cancelled') { + changes.push({ + row: rowNumber, + clientId: row.client_id, + action: row.action, + outcome: 'unchanged', + }); + return; + } + + mutations.push({ kind: 'delete', row, rowNumber, existing }); + finalById.set(existing.id, { ...existing, status: 'cancelled' }); + changes.push({ + row: rowNumber, + clientId: row.client_id, + action: row.action, + outcome: 'deleted', + }); + return; + } + + const values = valuesForRow(row, rowNumber, snapshot); + if (row.action === 'create') { + if (existing) { + if (!matchesValues(existing, values)) { + throw conflict('A create row reused a client_id with different session data', { + [`rows.${rowNumber}.client_id`]: row.client_id, + }); + } + changes.push({ + row: rowNumber, + clientId: row.client_id, + action: row.action, + outcome: 'unchanged', + }); + return; + } + + mutations.push({ kind: 'create', row, rowNumber, values }); + additions.push(provisional(values)); + changes.push({ + row: rowNumber, + clientId: row.client_id, + action: row.action, + outcome: 'created', + }); + return; + } + + if (!existing) { + throw conflict('An update row did not match a session in this event', { + [`rows.${rowNumber}.client_id`]: row.client_id, + }); + } + if (matchesValues(existing, values)) { + changes.push({ + row: rowNumber, + clientId: row.client_id, + action: row.action, + outcome: 'unchanged', + }); + return; + } + + mutations.push({ kind: 'update', row, rowNumber, existing, values }); + finalById.set(existing.id, withValues(existing, values)); + changes.push({ + row: rowNumber, + clientId: row.client_id, + action: row.action, + outcome: 'updated', + }); + }); + + const finalEntries = [...finalById.values(), ...additions]; + const counts = (outcome: SessionSyncPlan['changes'][number]['outcome']) => + changes.filter((change) => change.outcome === outcome).length; + const notificationsPlanned = mutations.filter( + (mutation) => + mutation.kind !== 'delete' || + (mutation.existing.status === 'published' && isPlaced(mutation.existing)), + ).length; + + return { + created: counts('created'), + updated: counts('updated'), + deleted: counts('deleted'), + unchanged: counts('unchanged'), + changes, + conflicts: detectConflicts(finalEntries, { + rooms: Object.fromEntries(snapshot.rooms.map((value) => [value.id, value.name])), + tracks: Object.fromEntries(snapshot.tracks.map((value) => [value.id, value.name])), + }).map(publicConflict), + mutations, + finalEntries, + notificationsPlanned, + }; +} + +type SessionPatch = Omit, 'status'> & { + status?: 'published' | 'cancelled'; +}; + +export type SessionSyncStore = { + transaction(work: (transaction: SessionSyncStore) => Promise): Promise; + loadSnapshot(eventId: string, lock: boolean): Promise; + reserveSessionRefs(eventId: string, count: number): Promise; + insertSession(eventId: string, ref: number, values: SessionValues): Promise; + updateSession(eventId: string, sessionId: string, patch: SessionPatch): Promise; +}; + +type DatabaseTransaction = Parameters[0]>[0]; +type DatabaseExecutor = Database | DatabaseTransaction; + +async function loadSnapshotFrom( + database: DatabaseExecutor, + eventId: string, + lock: boolean, +): Promise { + const eventRows = lock + ? await database + .select({ id: event.id }) + .from(event) + .where(eq(event.id, eventId)) + .for('update') + : await database.select({ id: event.id }).from(event).where(eq(event.id, eventId)); + if (eventRows.length === 0) throw notFound('That event'); + + const [sessionRows, roomRows, trackRows, formatRows] = await Promise.all([ + database.select().from(scheduledSession).where(eq(scheduledSession.eventId, eventId)), + database.select({ id: room.id, name: room.name }).from(room).where(eq(room.eventId, eventId)), + database.select({ id: track.id, name: track.name }).from(track).where(eq(track.eventId, eventId)), + database + .select({ id: sessionFormat.id, name: sessionFormat.name }) + .from(sessionFormat) + .where(eq(sessionFormat.eventId, eventId)), + ]); + + const submissionIds = sessionRows + .map((session) => session.submissionId) + .filter((submissionId): submissionId is string => Boolean(submissionId)); + const speakerRows = + submissionIds.length === 0 + ? [] + : await database + .select({ + submissionId: participantRole.submissionId, + participantId: participant.id, + displayName: participant.displayName, + userName: user.name, + email: user.email, + }) + .from(participantRole) + .innerJoin(participant, eq(participant.id, participantRole.participantId)) + .innerJoin(user, eq(user.id, participant.userId)) + .where(inArray(participantRole.submissionId, submissionIds)); + const speakers = new Map(); + for (const speaker of speakerRows) { + speakers.set(speaker.submissionId, [ + ...(speakers.get(speaker.submissionId) ?? []), + { + participantId: speaker.participantId, + name: speaker.displayName ?? speaker.userName ?? speaker.email, + }, + ]); + } + + return { + sessions: sessionRows.map((session) => ({ + id: session.id, + eventId: session.eventId, + ref: session.ref, + title: session.title, + submissionId: session.submissionId, + descriptionMarkdown: session.descriptionMarkdown, + roomId: session.roomId, + trackId: session.trackId, + formatId: session.formatId, + startsAt: session.startsAt, + endsAt: session.endsAt, + status: session.status, + ceuCredits: session.ceuCredits, + clientId: session.clientId, + icsUid: session.icsUid, + icsSequence: session.icsSequence, + speakers: session.submissionId ? (speakers.get(session.submissionId) ?? []) : [], + })), + rooms: roomRows, + tracks: trackRows, + formats: formatRows, + }; +} + +function drizzleStore( + database: DatabaseExecutor, + runTransaction?: (work: (transaction: SessionSyncStore) => Promise) => Promise, +): SessionSyncStore { + const store: SessionSyncStore = { + transaction: (work: (transaction: SessionSyncStore) => Promise) => + runTransaction ? runTransaction(work) : work(store), + loadSnapshot: (eventId, lock) => loadSnapshotFrom(database, eventId, lock), + reserveSessionRefs: async (eventId, count) => { + if (count === 0) return []; + const [row] = await database + .update(event) + .set({ sessionSeq: sql`${event.sessionSeq} + ${count}`, updatedAt: new Date() }) + .where(eq(event.id, eventId)) + .returning({ lastRef: event.sessionSeq }); + if (!row) throw notFound('That event'); + return Array.from({ length: count }, (_, index) => row.lastRef - count + index + 1); + }, + insertSession: async (eventId, ref, values) => { + const [created] = await database + .insert(scheduledSession) + .values({ + eventId, + ref, + submissionId: null, + ...values, + icsUid: newIcsUid(), + }) + .returning({ id: scheduledSession.id }); + if (!created) throw new Error('The session could not be created'); + return created.id; + }, + updateSession: async (eventId, sessionId, patch) => { + const [updated] = await database + .update(scheduledSession) + .set({ ...patch, updatedAt: new Date() }) + .where(and(eq(scheduledSession.id, sessionId), eq(scheduledSession.eventId, eventId))) + .returning({ id: scheduledSession.id }); + if (!updated) throw conflict('A session changed while this sync was being applied'); + }, + }; + return store; +} + +export function createSessionSyncStore(database: Database = getDb()): SessionSyncStore { + return drizzleStore(database, (work) => + database.transaction((transaction) => work(drizzleStore(transaction))), + ); +} + +type CalendarNotification = { sessionId: string; cancel: boolean }; + +export type SessionSyncOptions = { + dryRun: boolean; + store?: SessionSyncStore; + notify?: (sessionId: string, options: { cancel: boolean }) => Promise; +}; + +function uniqueViolation(error: unknown): boolean { + return ( + typeof error === 'object' && + error !== null && + 'code' in error && + (error as { code?: unknown }).code === '23505' + ); +} + +export async function syncPublishedSessions( + eventId: string, + rows: SessionSyncRow[], + options: SessionSyncOptions, +): Promise { + const store = options.store ?? createSessionSyncStore(); + if (options.dryRun) { + const plan = planSessionSync(rows, await store.loadSnapshot(eventId, false)); + return { + dryRun: true, + created: plan.created, + updated: plan.updated, + deleted: plan.deleted, + unchanged: plan.unchanged, + changes: plan.changes, + conflicts: plan.conflicts, + calendarNotifications: { + planned: plan.notificationsPlanned, + attempted: 0, + failed: 0, + }, + }; + } + + let committed: { plan: SessionSyncPlan; notifications: CalendarNotification[] }; + try { + committed = await store.transaction(async (transaction) => { + const plan = planSessionSync(rows, await transaction.loadSnapshot(eventId, true)); + const refs = await transaction.reserveSessionRefs(eventId, plan.created); + const notifications: CalendarNotification[] = []; + let refIndex = 0; + + for (const mutation of plan.mutations) { + if (mutation.kind === 'create') { + const sessionId = await transaction.insertSession( + eventId, + refs[refIndex] as number, + mutation.values, + ); + refIndex += 1; + notifications.push({ sessionId, cancel: false }); + continue; + } + if (mutation.kind === 'update') { + await transaction.updateSession(eventId, mutation.existing.id, mutation.values); + notifications.push({ sessionId: mutation.existing.id, cancel: false }); + continue; + } + + await transaction.updateSession(eventId, mutation.existing.id, { status: 'cancelled' }); + if (mutation.existing.status === 'published' && isPlaced(mutation.existing)) { + notifications.push({ sessionId: mutation.existing.id, cancel: true }); + } + } + return { plan, notifications }; + }); + } catch (error) { + if (uniqueViolation(error)) { + throw conflict('Another sync claimed one of those client IDs; preview the file again'); + } + throw error; + } + + const notify = + options.notify ?? + (async (sessionId: string, notification: { cancel: boolean }) => { + await sendSessionInvites(sessionId, notification.cancel ? { cancel: true } : {}); + }); + const notificationResults = await Promise.allSettled( + committed.notifications.map((notification) => + notify(notification.sessionId, { cancel: notification.cancel }), + ), + ); + const failed = notificationResults.filter((result) => result.status === 'rejected').length; + for (const result of notificationResults) { + if (result.status === 'rejected') { + console.error(result.reason instanceof Error ? result.reason.message : String(result.reason)); + } + } + + return { + dryRun: false, + created: committed.plan.created, + updated: committed.plan.updated, + deleted: committed.plan.deleted, + unchanged: committed.plan.unchanged, + changes: committed.plan.changes, + conflicts: committed.plan.conflicts, + calendarNotifications: { + planned: committed.plan.notificationsPlanned, + attempted: committed.notifications.length, + failed, + }, + }; +}