diff --git a/.trellis/spec/backend/drizzle-postgres-current.md b/.trellis/spec/backend/drizzle-postgres-current.md index 424797a..674e81a 100644 --- a/.trellis/spec/backend/drizzle-postgres-current.md +++ b/.trellis/spec/backend/drizzle-postgres-current.md @@ -111,6 +111,107 @@ await database.transaction(async (transaction) => { }); ``` +## Scenario: Organization Invitation Limits + +### 1. Scope / Trigger + +- Trigger: changing organization invitation creation, registration-by-invite consumption, or the `organization_invitations` table. +- Applies because invitation links are persisted in PostgreSQL, created through a protected organization API, consumed inside account registration, and displayed in organization settings. + +### 2. Signatures + +- `POST /api/organizations/[id]/invitations` +- Request: `{ email?: string; roleId: string; validityDays?: number; maxUses?: number }` +- Response: `ApiResult<{ invitation: typeof organizationInvitations.$inferSelect }>` +- DB columns: + - `organization_invitations.expires_at timestamp with time zone not null` + - `organization_invitations.max_uses integer not null default 1` + - `organization_invitations.used_count integer not null default 0` +- Registration helper: `createRegistration({ name, email, password, organizationId?, invitationToken? })` + +### 3. Contracts + +- Invite creation requires `account:manage` and may only target the active organization when the session is organization-scoped. +- `roleId` must refer to an enabled role in the target organization. +- If invitation `email` is non-empty, registration must use the same normalized email address. +- `validityDays` defaults to `7` and must be a positive integer within the product limit. +- `maxUses` defaults to `1` and must be a positive integer within the product limit. +- "Use count" means successful invitation registrations, not page visits or token preview requests. +- A token is consumable only when `status = 'active'`, `expires_at >= now`, and `used_count < max_uses`. +- Successful registration inserts the account and membership in the same transaction before consuming the invitation. +- Consuming an invitation increments `used_count`; only when the returned count reaches `max_uses` should the row become `status = 'accepted'` and receive `accepted_by_account_id` / `accepted_at`. + +### 4. Validation & Error Matrix + +- Invalid JSON body -> `400` / `请求数据格式无效`. +- Missing `roleId` -> `400` / `请选择邀请角色`. +- Invalid `validityDays` -> `400` / `邀请有效期需为 ... 的整数`. +- Invalid `maxUses` -> `400` / `最大使用次数需为 ... 的整数`. +- Target organization missing -> `404` / `机构不存在`. +- Role missing, disabled, or cross-organization -> `404` / `角色不存在`. +- Unknown invitation token during registration -> `邀请链接无效`. +- Expired, non-active, or exhausted invitation token during registration -> `邀请链接已失效`. +- Registration email mismatch for an email-limited invitation -> `邀请邮箱与注册邮箱不一致`. + +### 5. Good/Base/Bad Cases + +- Good: keep invitation limit constants shared between the create API and invite dialog so UI bounds and backend validation stay aligned. +- Good: consume invitation links with an atomic `UPDATE ... WHERE used_count < max_uses` predicate and check that a row was returned. +- Good: derive invitation list UI from the persisted `used_count / max_uses` fields. +- Base: old one-use behavior remains the default by using `validityDays = 7` and `maxUses = 1`. +- Bad: count page loads as invitation usage; link previews, refreshes, and bots can exhaust a link without a registration. +- Bad: decide whether a link is exhausted only from a value read before registration; concurrent registrations can over-consume the link. + +### 6. Tests Required + +- `pnpm db:generate`, then review SQL for only additive invitation limit columns or intentional invitation changes. +- `pnpm lint` +- `pnpm type-check` +- `pnpm test` +- `pnpm build` +- Integration assertions when route-level tests cover auth registration: + - default invite creation returns a 7-day, one-use link + - custom `validityDays` and `maxUses` are persisted + - expired and exhausted links fail registration + - successful registration increments `used_count` + - the final allowed registration marks the invitation accepted + +### 7. Wrong vs Correct + +#### Wrong + +```ts +if (invitation.usedCount >= invitation.maxUses) { + throw new Error("邀请链接已失效"); +} +await transaction.update(organizationInvitations) + .set({ usedCount: invitation.usedCount + 1 }) + .where(eq(organizationInvitations.id, invitation.id)); +``` + +#### Correct + +```ts +const consumedRows = await transaction + .update(organizationInvitations) + .set({ usedCount: sql`${organizationInvitations.usedCount} + 1` }) + .where(and( + eq(organizationInvitations.id, invitation.id), + eq(organizationInvitations.status, "active"), + gte(organizationInvitations.expiresAt, now), + lt(organizationInvitations.usedCount, organizationInvitations.maxUses), + )) + .returning({ + id: organizationInvitations.id, + maxUses: organizationInvitations.maxUses, + usedCount: organizationInvitations.usedCount, + }); +const consumed = consumedRows[0]; +if (!consumed) { + throw new Error("邀请链接已失效"); +} +``` + ## Scenario: Facility Room Creation Without Fabricated Hierarchy ### 1. Scope / Trigger diff --git a/.trellis/tasks/07-03-invite-link-limits/check.jsonl b/.trellis/tasks/07-03-invite-link-limits/check.jsonl new file mode 100644 index 0000000..9dd3234 --- /dev/null +++ b/.trellis/tasks/07-03-invite-link-limits/check.jsonl @@ -0,0 +1 @@ +{"_example": "Fill with {\"file\": \"\", \"reason\": \"\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."} diff --git a/.trellis/tasks/07-03-invite-link-limits/design.md b/.trellis/tasks/07-03-invite-link-limits/design.md new file mode 100644 index 0000000..a9f8ae2 --- /dev/null +++ b/.trellis/tasks/07-03-invite-link-limits/design.md @@ -0,0 +1,82 @@ +# Invite Link Limits Design + +## Architecture + +This change spans the persisted invitation model, the invitation create API, the registration consumption transaction, and the organization settings UI. + +The invitation row remains the source of truth. The existing `expires_at` column continues to represent validity. New persisted counters will represent usage limits: + +- `max_uses`: maximum successful invitation registrations allowed. +- `used_count`: number of successful registrations that have consumed the invitation. + +## Data Model + +Add non-null integer columns to `organization_invitations`: + +- `max_uses integer not null default 1` +- `used_count integer not null default 0` + +Existing invitation rows remain compatible: previous one-time invites become `max_uses = 1`, `used_count = 0` unless already accepted. The existing `status = accepted` already prevents use of accepted legacy rows. + +## Invitation Creation Contract + +`POST /api/organizations/[id]/invitations` accepts: + +- `email?: string` +- `roleId: string` +- `validityDays?: number` +- `maxUses?: number` + +Defaults: + +- `validityDays = 7` +- `maxUses = 1` + +Validation: + +- `roleId` is still required. +- `validityDays` must be a positive integer in an operationally reasonable range. +- `maxUses` must be a positive integer in an operationally reasonable range. +- Invalid input returns `jsonFailure(...)` with the existing structured API shape. + +## Invitation Consumption + +Registration only counts a use after account creation and membership insertion succeed inside the existing transaction. + +An invitation is valid only if: + +- `status === "active"` +- `expiresAt >= now` +- `usedCount < maxUses` + +On successful invitation registration: + +- Increment `usedCount`. +- If the increment reaches `maxUses`, set `status = "accepted"` and fill `acceptedByAccountId` / `acceptedAt` with the consuming account details. +- If remaining uses exist, keep `status = "active"` and leave accepted metadata empty. + +The update should guard against concurrent over-consumption by updating with a `used_count < max_uses` predicate and checking that a row was returned. + +## UI + +`OrganizationInviteDialog` adds controls for: + +- Validity days, default 7. +- Maximum uses, default 1. + +`OrganizationDetailClient` displays: + +- Expiry time. +- Usage as `usedCount / maxUses`. + +Copy behavior remains unchanged. + +## Compatibility + +The existing `/register?invite=...` URL format remains unchanged. + +Existing rows without the new columns are migrated through default values. Existing one-time semantics are preserved by default. + +## Rollback + +If needed, UI inputs can be hidden and backend defaults can keep the old behavior. Dropping the columns would require a reverse migration only if deployed migrations must be rolled back. diff --git a/.trellis/tasks/07-03-invite-link-limits/implement.jsonl b/.trellis/tasks/07-03-invite-link-limits/implement.jsonl new file mode 100644 index 0000000..9dd3234 --- /dev/null +++ b/.trellis/tasks/07-03-invite-link-limits/implement.jsonl @@ -0,0 +1 @@ +{"_example": "Fill with {\"file\": \"\", \"reason\": \"\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."} diff --git a/.trellis/tasks/07-03-invite-link-limits/implement.md b/.trellis/tasks/07-03-invite-link-limits/implement.md new file mode 100644 index 0000000..fda02bb --- /dev/null +++ b/.trellis/tasks/07-03-invite-link-limits/implement.md @@ -0,0 +1,42 @@ +# Invite Link Limits Implementation Plan + +## Checklist + +1. Update Trellis planning artifacts and get approval to start implementation. +2. Load pre-development specs with `trellis-before-dev`. +3. Update `modules/core/server/schema.ts`: + - Add `maxUses` and `usedCount` integer columns to `organizationInvitations`. +4. Generate or add the Drizzle migration for the new columns. +5. Update shared invitation types and row mappers: + - `modules/core/types.ts` + - `modules/core/server/settings.ts` + - `modules/core/server/store.ts` +6. Update invitation creation API: + - Parse `validityDays` and `maxUses`. + - Validate positive integer ranges. + - Persist `expiresAt`, `maxUses`, and `usedCount`. +7. Update invitation registration flow: + - Reject exhausted links. + - Increment usage inside the transaction. + - Mark status accepted only when `usedCount` reaches `maxUses`. + - Guard concurrent consumption with an update predicate. +8. Update frontend: + - Add validity and maximum-use controls to `OrganizationInviteDialog`. + - Display usage count in `OrganizationDetailClient`. + - Update invitation explanatory text where it still says invitations are one-time only. +9. Run validation: + - `pnpm lint` + - `pnpm type-check` + - `pnpm test` if tests are relevant or fast enough after type changes. + - `pnpm build` if lint/type-check pass. + +## Risk Points + +- Concurrent registrations could over-consume an invite unless the update predicate checks `used_count < max_uses`. +- Existing accepted invitations should remain unusable regardless of migrated counter defaults. +- Frontend numeric inputs submit strings, so backend parsing must not trust the client. + +## Rollback Points + +- Before migration: schema/API/UI changes can be reverted together. +- After migration: reverting code while keeping columns is safe because defaults preserve one-use invite behavior. diff --git a/.trellis/tasks/07-03-invite-link-limits/prd.md b/.trellis/tasks/07-03-invite-link-limits/prd.md new file mode 100644 index 0000000..dfa7102 --- /dev/null +++ b/.trellis/tasks/07-03-invite-link-limits/prd.md @@ -0,0 +1,48 @@ +# Invite link limits + +## Goal + +Allow organization admins to configure invitation link validity and usage limits when creating invite links. + +This should make invite links safer to distribute by letting admins choose how long a link remains valid and how many times it may be used. + +## Confirmed Facts + +- Invitation links are created from `modules/settings/components/OrganizationInviteDialog.tsx`. +- The invitation create API is `app/api/organizations/[id]/invitations/route.ts`. +- Invitation persistence is backed by the `organization_invitations` table in `modules/core/server/schema.ts`. +- Invitations already have an `expiresAt` column and are currently created with a fixed 7-day validity window. +- Registration consumes invitation tokens through `modules/core/server/auth.ts`. +- The current registration flow marks an invitation as `accepted` after one successful invitation registration, so existing invite links behave as single-use links. +- Invitation rows are shown in `modules/settings/components/OrganizationDetailClient.tsx`, currently with target, role, status, expiry, and copy action. + +## Requirements + +- Admins can configure an invitation link's validity period when creating an organization invitation. +- Admins can configure the maximum usage count for an invitation link when creating an organization invitation. +- Existing behavior remains compatible by default: if the admin does not change the controls, a new invite should expire after 7 days and be usable once. +- The backend validates create-invitation inputs and rejects invalid limit values with structured API failures. +- The registration path rejects expired or exhausted invitation links. +- Invitation list data exposes enough information to show configured limits and current usage. +- The UI makes the configured validity and usage limit visible when creating and reviewing invitations. +- Maximum access count means maximum successful invitation registrations, not page loads or token preview requests. + +## Out of Scope + +- Invitation revocation management beyond existing status behavior. +- Editing limits after an invitation has been created. +- Tracking anonymous page views unless product intent explicitly requires page-load based access counting. + +## Acceptance Criteria + +- [x] Creating an invitation with default form values produces a 7-day, one-use invitation. +- [x] Creating an invitation with a custom validity period stores and displays the corresponding expiry. +- [x] Creating an invitation with a custom maximum usage count stores and displays the configured usage limit. +- [x] Registration with an expired invitation fails with the existing invalid/expired invitation behavior. +- [x] Registration after the invitation has reached its maximum allowed uses fails. +- [x] Successful invitation registration increments usage accounting and only exhausts the link when the configured maximum is reached. +- [x] Type-check, lint, and relevant tests/build checks pass. + +## Notes + +- Recommended interpretation: count successful invitation registrations as usage. Counting page loads would make links vulnerable to browser refreshes, previews, bots, and accidental visits, and it would require adding a separate token validation/view endpoint. diff --git a/.trellis/tasks/07-03-invite-link-limits/task.json b/.trellis/tasks/07-03-invite-link-limits/task.json new file mode 100644 index 0000000..27e8f52 --- /dev/null +++ b/.trellis/tasks/07-03-invite-link-limits/task.json @@ -0,0 +1,26 @@ +{ + "id": "invite-link-limits", + "name": "invite-link-limits", + "title": "Invite link limits", + "description": "", + "status": "in_progress", + "dev_type": null, + "scope": null, + "package": null, + "priority": "P2", + "creator": "TalexDreamSoul", + "assignee": "TalexDreamSoul", + "createdAt": "2026-07-03", + "completedAt": null, + "branch": null, + "base_branch": "main", + "worktree_path": null, + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [], + "notes": "", + "meta": {} +} \ No newline at end of file diff --git a/app/api/organizations/[id]/invitations/route.ts b/app/api/organizations/[id]/invitations/route.ts index cb946a7..86202df 100644 --- a/app/api/organizations/[id]/invitations/route.ts +++ b/app/api/organizations/[id]/invitations/route.ts @@ -2,6 +2,14 @@ import { randomBytes } from "node:crypto"; import { and, eq } from "drizzle-orm"; +import { + DEFAULT_INVITATION_MAX_USES, + DEFAULT_INVITATION_VALIDITY_DAYS, + MAX_INVITATION_MAX_USES, + MAX_INVITATION_VALIDITY_DAYS, + MIN_INVITATION_MAX_USES, + MIN_INVITATION_VALIDITY_DAYS, +} from "@/modules/core/invitation-limits"; import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api"; import { recordAuditLog } from "@/modules/core/server/audit"; import { requirePermission } from "@/modules/core/server/auth"; @@ -23,6 +31,25 @@ function readString(source: Record, key: string): string { return typeof value === "string" ? value.trim() : ""; } +function readBoundedInteger( + source: Record, + key: string, + options: { + defaultValue: number; + min: number; + max: number; + label: string; + }, +): { success: true; value: number } | { success: false; reason: string } { + const rawValue = source[key]; + const value = rawValue === undefined || rawValue === null ? options.defaultValue : Number(rawValue); + if (!Number.isInteger(value) || value < options.min || value > options.max) { + return { success: false, reason: `${options.label}需为 ${options.min}-${options.max} 的整数` }; + } + + return { success: true, value }; +} + function createInvitationToken(): string { return randomBytes(24).toString("base64url"); } @@ -55,6 +82,26 @@ export async function POST(request: Request, context: RouteContext): Promise statement-breakpoint +ALTER TABLE "organization_invitations" ADD COLUMN "used_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint +UPDATE "organization_invitations" SET "used_count" = "max_uses" WHERE "status" <> 'active'; diff --git a/drizzle/meta/0006_snapshot.json b/drizzle/meta/0006_snapshot.json new file mode 100644 index 0000000..c751392 --- /dev/null +++ b/drizzle/meta/0006_snapshot.json @@ -0,0 +1,4510 @@ +{ + "id": "ce06f38d-8c68-47d5-8584-1ff0322423cf", + "prevId": "2ef6c9ae-30f3-48bd-95af-9103ba70baf6", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "platform_role_id": { + "name": "platform_role_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_salt": { + "name": "password_salt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "account_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "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": { + "accounts_email_unique": { + "name": "accounts_email_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_platform_role_id_roles_id_fk": { + "name": "accounts_platform_role_id_roles_id_fk", + "tableFrom": "accounts", + "tableTo": "roles", + "columnsFrom": [ + "platform_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.admissions": { + "name": "admissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "elder_id": { + "name": "elder_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "bed_id": { + "name": "bed_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "admission_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "admitted_at": { + "name": "admitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "discharged_at": { + "name": "discharged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "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": { + "admissions_organization_id_organizations_id_fk": { + "name": "admissions_organization_id_organizations_id_fk", + "tableFrom": "admissions", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "admissions_elder_id_elders_id_fk": { + "name": "admissions_elder_id_elders_id_fk", + "tableFrom": "admissions", + "tableTo": "elders", + "columnsFrom": [ + "elder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "admissions_bed_id_beds_id_fk": { + "name": "admissions_bed_id_beds_id_fk", + "tableFrom": "admissions", + "tableTo": "beds", + "columnsFrom": [ + "bed_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rules": { + "name": "alert_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rule_type": { + "name": "rule_type", + "type": "alert_rule_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'other'" + }, + "severity": { + "name": "severity", + "type": "incident_severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'warning'" + }, + "status": { + "name": "status", + "type": "alert_rule_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'enabled'" + }, + "condition_summary": { + "name": "condition_summary", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "suggestion": { + "name": "suggestion", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "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": { + "alert_rules_status_lookup": { + "name": "alert_rules_status_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rule_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "alert_rules_organization_id_organizations_id_fk": { + "name": "alert_rules_organization_id_organizations_id_fk", + "tableFrom": "alert_rules", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_triggers": { + "name": "alert_triggers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "elder_id": { + "name": "elder_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "alert_trigger_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "handled_by_account_id": { + "name": "handled_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "handled_at": { + "name": "handled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "handling_notes": { + "name": "handling_notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "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": { + "alert_triggers_queue_lookup": { + "name": "alert_triggers_queue_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alert_triggers_rule_lookup": { + "name": "alert_triggers_rule_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "alert_triggers_organization_id_organizations_id_fk": { + "name": "alert_triggers_organization_id_organizations_id_fk", + "tableFrom": "alert_triggers", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_triggers_rule_id_alert_rules_id_fk": { + "name": "alert_triggers_rule_id_alert_rules_id_fk", + "tableFrom": "alert_triggers", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "alert_triggers_elder_id_elders_id_fk": { + "name": "alert_triggers_elder_id_elders_id_fk", + "tableFrom": "alert_triggers", + "tableTo": "elders", + "columnsFrom": [ + "elder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "alert_triggers_handled_by_account_id_accounts_id_fk": { + "name": "alert_triggers_handled_by_account_id_accounts_id_fk", + "tableFrom": "alert_triggers", + "tableTo": "accounts", + "columnsFrom": [ + "handled_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "actor_account_id": { + "name": "actor_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "audit_result", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_organization_id_organizations_id_fk": { + "name": "audit_logs_organization_id_organizations_id_fk", + "tableFrom": "audit_logs", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_logs_actor_account_id_accounts_id_fk": { + "name": "audit_logs_actor_account_id_accounts_id_fk", + "tableFrom": "audit_logs", + "tableTo": "accounts", + "columnsFrom": [ + "actor_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beds": { + "name": "beds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "bed_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'available'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "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": { + "beds_code_room_unique": { + "name": "beds_code_room_unique", + "columns": [ + { + "expression": "room_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beds_organization_id_organizations_id_fk": { + "name": "beds_organization_id_organizations_id_fk", + "tableFrom": "beds", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "beds_room_id_rooms_id_fk": { + "name": "beds_room_id_rooms_id_fk", + "tableFrom": "beds", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.buildings": { + "name": "buildings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "campus_id": { + "name": "campus_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "buildings_organization_id_organizations_id_fk": { + "name": "buildings_organization_id_organizations_id_fk", + "tableFrom": "buildings", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "buildings_campus_id_campuses_id_fk": { + "name": "buildings_campus_id_campuses_id_fk", + "tableFrom": "buildings", + "tableTo": "campuses", + "columnsFrom": [ + "campus_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.campuses": { + "name": "campuses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "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": { + "campuses_organization_id_organizations_id_fk": { + "name": "campuses_organization_id_organizations_id_fk", + "tableFrom": "campuses", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.care_tasks": { + "name": "care_tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "elder_id": { + "name": "elder_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "care_type": { + "name": "care_type", + "type": "care_task_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "care_task_priority", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "status": { + "name": "status", + "type": "care_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "assignee_label": { + "name": "assignee_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "execution_notes": { + "name": "execution_notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "completed_by_account_id": { + "name": "completed_by_account_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": { + "care_tasks_queue_lookup": { + "name": "care_tasks_queue_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "care_tasks_schedule_lookup": { + "name": "care_tasks_schedule_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "care_tasks_organization_id_organizations_id_fk": { + "name": "care_tasks_organization_id_organizations_id_fk", + "tableFrom": "care_tasks", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "care_tasks_elder_id_elders_id_fk": { + "name": "care_tasks_elder_id_elders_id_fk", + "tableFrom": "care_tasks", + "tableTo": "elders", + "columnsFrom": [ + "elder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "care_tasks_created_by_account_id_accounts_id_fk": { + "name": "care_tasks_created_by_account_id_accounts_id_fk", + "tableFrom": "care_tasks", + "tableTo": "accounts", + "columnsFrom": [ + "created_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "care_tasks_completed_by_account_id_accounts_id_fk": { + "name": "care_tasks_completed_by_account_id_accounts_id_fk", + "tableFrom": "care_tasks", + "tableTo": "accounts", + "columnsFrom": [ + "completed_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chronic_conditions": { + "name": "chronic_conditions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "elder_id": { + "name": "elder_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "chronic_condition_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "diagnosed_at": { + "name": "diagnosed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "treatment_notes": { + "name": "treatment_notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "follow_up_notes": { + "name": "follow_up_notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "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": { + "chronic_conditions_elder_status_lookup": { + "name": "chronic_conditions_elder_status_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "elder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chronic_conditions_organization_id_organizations_id_fk": { + "name": "chronic_conditions_organization_id_organizations_id_fk", + "tableFrom": "chronic_conditions", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chronic_conditions_elder_id_elders_id_fk": { + "name": "chronic_conditions_elder_id_elders_id_fk", + "tableFrom": "chronic_conditions", + "tableTo": "elders", + "columnsFrom": [ + "elder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_assets": { + "name": "device_assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "device_asset_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_inspected_at": { + "name": "last_inspected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "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": { + "device_assets_code_organization_unique": { + "name": "device_assets_code_organization_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_assets_status_lookup": { + "name": "device_assets_status_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_assets_organization_id_organizations_id_fk": { + "name": "device_assets_organization_id_organizations_id_fk", + "tableFrom": "device_assets", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.elders": { + "name": "elders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "gender": { + "name": "gender", + "type": "gender", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "age": { + "name": "age", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "care_level": { + "name": "care_level", + "type": "care_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "elder_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "primary_contact": { + "name": "primary_contact", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "medical_notes": { + "name": "medical_notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "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": { + "elders_organization_id_organizations_id_fk": { + "name": "elders_organization_id_organizations_id_fk", + "tableFrom": "elders", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.family_contacts": { + "name": "family_contacts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "elder_id": { + "name": "elder_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "relationship": { + "name": "relationship", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "family_contact_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "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": { + "family_contacts_elder_lookup": { + "name": "family_contacts_elder_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "elder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "family_contacts_organization_id_organizations_id_fk": { + "name": "family_contacts_organization_id_organizations_id_fk", + "tableFrom": "family_contacts", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "family_contacts_elder_id_elders_id_fk": { + "name": "family_contacts_elder_id_elders_id_fk", + "tableFrom": "family_contacts", + "tableTo": "elders", + "columnsFrom": [ + "elder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.family_feedback": { + "name": "family_feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "elder_id": { + "name": "elder_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "feedback_type": { + "name": "feedback_type", + "type": "family_feedback_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'other'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "family_feedback_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "response_notes": { + "name": "response_notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "handled_by_account_id": { + "name": "handled_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "handled_at": { + "name": "handled_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": { + "family_feedback_queue_lookup": { + "name": "family_feedback_queue_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "feedback_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "family_feedback_organization_id_organizations_id_fk": { + "name": "family_feedback_organization_id_organizations_id_fk", + "tableFrom": "family_feedback", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "family_feedback_elder_id_elders_id_fk": { + "name": "family_feedback_elder_id_elders_id_fk", + "tableFrom": "family_feedback", + "tableTo": "elders", + "columnsFrom": [ + "elder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "family_feedback_contact_id_family_contacts_id_fk": { + "name": "family_feedback_contact_id_family_contacts_id_fk", + "tableFrom": "family_feedback", + "tableTo": "family_contacts", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "family_feedback_handled_by_account_id_accounts_id_fk": { + "name": "family_feedback_handled_by_account_id_accounts_id_fk", + "tableFrom": "family_feedback", + "tableTo": "accounts", + "columnsFrom": [ + "handled_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.family_visit_appointments": { + "name": "family_visit_appointments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "elder_id": { + "name": "elder_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "family_visit_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'requested'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "handled_by_account_id": { + "name": "handled_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "handled_at": { + "name": "handled_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": { + "family_visit_appointments_schedule_lookup": { + "name": "family_visit_appointments_schedule_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "family_visit_appointments_organization_id_organizations_id_fk": { + "name": "family_visit_appointments_organization_id_organizations_id_fk", + "tableFrom": "family_visit_appointments", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "family_visit_appointments_elder_id_elders_id_fk": { + "name": "family_visit_appointments_elder_id_elders_id_fk", + "tableFrom": "family_visit_appointments", + "tableTo": "elders", + "columnsFrom": [ + "elder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "family_visit_appointments_contact_id_family_contacts_id_fk": { + "name": "family_visit_appointments_contact_id_family_contacts_id_fk", + "tableFrom": "family_visit_appointments", + "tableTo": "family_contacts", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "family_visit_appointments_handled_by_account_id_accounts_id_fk": { + "name": "family_visit_appointments_handled_by_account_id_accounts_id_fk", + "tableFrom": "family_visit_appointments", + "tableTo": "accounts", + "columnsFrom": [ + "handled_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.floors": { + "name": "floors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "building_id": { + "name": "building_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "floors_organization_id_organizations_id_fk": { + "name": "floors_organization_id_organizations_id_fk", + "tableFrom": "floors", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "floors_building_id_buildings_id_fk": { + "name": "floors_building_id_buildings_id_fk", + "tableFrom": "floors", + "tableTo": "buildings", + "columnsFrom": [ + "building_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.health_anomaly_reviews": { + "name": "health_anomaly_reviews", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "elder_id": { + "name": "elder_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "vital_record_id": { + "name": "vital_record_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "severity": { + "name": "severity", + "type": "incident_severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "health_review_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reviewed_by_account_id": { + "name": "reviewed_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolution_notes": { + "name": "resolution_notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "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": { + "health_anomaly_reviews_queue_lookup": { + "name": "health_anomaly_reviews_queue_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "severity", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "health_anomaly_reviews_organization_id_organizations_id_fk": { + "name": "health_anomaly_reviews_organization_id_organizations_id_fk", + "tableFrom": "health_anomaly_reviews", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "health_anomaly_reviews_elder_id_elders_id_fk": { + "name": "health_anomaly_reviews_elder_id_elders_id_fk", + "tableFrom": "health_anomaly_reviews", + "tableTo": "elders", + "columnsFrom": [ + "elder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "health_anomaly_reviews_vital_record_id_vital_records_id_fk": { + "name": "health_anomaly_reviews_vital_record_id_vital_records_id_fk", + "tableFrom": "health_anomaly_reviews", + "tableTo": "vital_records", + "columnsFrom": [ + "vital_record_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "health_anomaly_reviews_reviewed_by_account_id_accounts_id_fk": { + "name": "health_anomaly_reviews_reviewed_by_account_id_accounts_id_fk", + "tableFrom": "health_anomaly_reviews", + "tableTo": "accounts", + "columnsFrom": [ + "reviewed_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.health_profiles": { + "name": "health_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "elder_id": { + "name": "elder_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "allergy_notes": { + "name": "allergy_notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "medical_history": { + "name": "medical_history", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "medication_notes": { + "name": "medication_notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "care_restrictions": { + "name": "care_restrictions", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "emergency_notes": { + "name": "emergency_notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "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": { + "health_profiles_organization_elder_unique": { + "name": "health_profiles_organization_elder_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "elder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "health_profiles_organization_id_organizations_id_fk": { + "name": "health_profiles_organization_id_organizations_id_fk", + "tableFrom": "health_profiles", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "health_profiles_elder_id_elders_id_fk": { + "name": "health_profiles_elder_id_elders_id_fk", + "tableFrom": "health_profiles", + "tableTo": "elders", + "columnsFrom": [ + "elder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.join_requests": { + "name": "join_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "join_request_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "reviewed_by_account_id": { + "name": "reviewed_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_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": { + "join_requests_account_id_accounts_id_fk": { + "name": "join_requests_account_id_accounts_id_fk", + "tableFrom": "join_requests", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "join_requests_organization_id_organizations_id_fk": { + "name": "join_requests_organization_id_organizations_id_fk", + "tableFrom": "join_requests", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "join_requests_reviewed_by_account_id_accounts_id_fk": { + "name": "join_requests_reviewed_by_account_id_accounts_id_fk", + "tableFrom": "join_requests", + "tableTo": "accounts", + "columnsFrom": [ + "reviewed_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.maintenance_tickets": { + "name": "maintenance_tickets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "device_id": { + "name": "device_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "priority": { + "name": "priority", + "type": "maintenance_ticket_priority", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "status": { + "name": "status", + "type": "maintenance_ticket_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "assignee_label": { + "name": "assignee_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "resolution_notes": { + "name": "resolution_notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "resolved_at": { + "name": "resolved_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": { + "maintenance_tickets_queue_lookup": { + "name": "maintenance_tickets_queue_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "maintenance_tickets_device_lookup": { + "name": "maintenance_tickets_device_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "maintenance_tickets_organization_id_organizations_id_fk": { + "name": "maintenance_tickets_organization_id_organizations_id_fk", + "tableFrom": "maintenance_tickets", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "maintenance_tickets_device_id_device_assets_id_fk": { + "name": "maintenance_tickets_device_id_device_assets_id_fk", + "tableFrom": "maintenance_tickets", + "tableTo": "device_assets", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memberships": { + "name": "memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "membership_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "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": { + "memberships_account_organization_unique": { + "name": "memberships_account_organization_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memberships_account_id_accounts_id_fk": { + "name": "memberships_account_id_accounts_id_fk", + "tableFrom": "memberships", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memberships_organization_id_organizations_id_fk": { + "name": "memberships_organization_id_organizations_id_fk", + "tableFrom": "memberships", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memberships_role_id_roles_id_fk": { + "name": "memberships_role_id_roles_id_fk", + "tableFrom": "memberships", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notice_read_receipts": { + "name": "notice_read_receipts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "notice_id": { + "name": "notice_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "read_at": { + "name": "read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notice_read_receipts_account_notice_unique": { + "name": "notice_read_receipts_account_notice_unique", + "columns": [ + { + "expression": "notice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notice_read_receipts_notice_lookup": { + "name": "notice_read_receipts_notice_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "notice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notice_read_receipts_organization_id_organizations_id_fk": { + "name": "notice_read_receipts_organization_id_organizations_id_fk", + "tableFrom": "notice_read_receipts", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notice_read_receipts_notice_id_notices_id_fk": { + "name": "notice_read_receipts_notice_id_notices_id_fk", + "tableFrom": "notice_read_receipts", + "tableTo": "notices", + "columnsFrom": [ + "notice_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notice_read_receipts_account_id_accounts_id_fk": { + "name": "notice_read_receipts_account_id_accounts_id_fk", + "tableFrom": "notice_read_receipts", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notices": { + "name": "notices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'全体员工'" + }, + "status": { + "name": "status", + "type": "notice_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_account_id": { + "name": "updated_by_account_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": { + "notices_status_lookup": { + "name": "notices_status_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notices_organization_id_organizations_id_fk": { + "name": "notices_organization_id_organizations_id_fk", + "tableFrom": "notices", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notices_created_by_account_id_accounts_id_fk": { + "name": "notices_created_by_account_id_accounts_id_fk", + "tableFrom": "notices", + "tableTo": "accounts", + "columnsFrom": [ + "created_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "notices_updated_by_account_id_accounts_id_fk": { + "name": "notices_updated_by_account_id_accounts_id_fk", + "tableFrom": "notices", + "tableTo": "accounts", + "columnsFrom": [ + "updated_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_invitations": { + "name": "organization_invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "accepted_by_account_id": { + "name": "accepted_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "max_uses": { + "name": "max_uses", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "used_count": { + "name": "used_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_invitations_token_unique": { + "name": "organization_invitations_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_invitations_organization_id_organizations_id_fk": { + "name": "organization_invitations_organization_id_organizations_id_fk", + "tableFrom": "organization_invitations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_invitations_role_id_roles_id_fk": { + "name": "organization_invitations_role_id_roles_id_fk", + "tableFrom": "organization_invitations", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "organization_invitations_created_by_account_id_accounts_id_fk": { + "name": "organization_invitations_created_by_account_id_accounts_id_fk", + "tableFrom": "organization_invitations", + "tableTo": "accounts", + "columnsFrom": [ + "created_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "organization_invitations_accepted_by_account_id_accounts_id_fk": { + "name": "organization_invitations_accepted_by_account_id_accounts_id_fk", + "tableFrom": "organization_invitations", + "tableTo": "accounts", + "columnsFrom": [ + "accepted_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "organization_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "registration_enabled": { + "name": "registration_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "oidc_enabled": { + "name": "oidc_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_issuer_url": { + "name": "oidc_issuer_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_client_id": { + "name": "oidc_client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_client_secret": { + "name": "oidc_client_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_scopes": { + "name": "oidc_scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'openid profile email'" + }, + "oidc_redirect_uri": { + "name": "oidc_redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_avatar_claim": { + "name": "oidc_avatar_claim", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'picture'" + }, + "oidc_auto_provision": { + "name": "oidc_auto_provision", + "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": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_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()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role_permissions": { + "name": "role_permissions", + "schema": "", + "columns": { + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "permission_id": { + "name": "permission_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "role_permissions_role_id_roles_id_fk": { + "name": "role_permissions_role_id_roles_id_fk", + "tableFrom": "role_permissions", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "role_permissions_permission_id_permissions_id_fk": { + "name": "role_permissions_permission_id_permissions_id_fk", + "tableFrom": "role_permissions", + "tableTo": "permissions", + "columnsFrom": [ + "permission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "role_permissions_role_id_permission_id_pk": { + "name": "role_permissions_role_id_permission_id_pk", + "columns": [ + "role_id", + "permission_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "role_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_enabled": { + "name": "is_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": { + "roles_key_organization_unique": { + "name": "roles_key_organization_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "roles_organization_id_organizations_id_fk": { + "name": "roles_organization_id_organizations_id_fk", + "tableFrom": "roles", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rooms": { + "name": "rooms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "floor_id": { + "name": "floor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "capacity": { + "name": "capacity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "status": { + "name": "status", + "type": "organization_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "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": { + "rooms_code_organization_unique": { + "name": "rooms_code_organization_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "rooms_organization_id_organizations_id_fk": { + "name": "rooms_organization_id_organizations_id_fk", + "tableFrom": "rooms", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rooms_floor_id_floors_id_fk": { + "name": "rooms_floor_id_floors_id_fk", + "tableFrom": "rooms", + "tableTo": "floors", + "columnsFrom": [ + "floor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_account_id_accounts_id_fk": { + "name": "sessions_account_id_accounts_id_fk", + "tableFrom": "sessions", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_active_organization_id_organizations_id_fk": { + "name": "sessions_active_organization_id_organizations_id_fk", + "tableFrom": "sessions", + "tableTo": "organizations", + "columnsFrom": [ + "active_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_incidents": { + "name": "system_incidents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "severity": { + "name": "severity", + "type": "incident_severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "incident_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "acknowledged_by_account_id": { + "name": "acknowledged_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "acknowledged_at": { + "name": "acknowledged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolved_by_account_id": { + "name": "resolved_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_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": { + "system_incidents_organization_id_organizations_id_fk": { + "name": "system_incidents_organization_id_organizations_id_fk", + "tableFrom": "system_incidents", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "system_incidents_acknowledged_by_account_id_accounts_id_fk": { + "name": "system_incidents_acknowledged_by_account_id_accounts_id_fk", + "tableFrom": "system_incidents", + "tableTo": "accounts", + "columnsFrom": [ + "acknowledged_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "system_incidents_resolved_by_account_id_accounts_id_fk": { + "name": "system_incidents_resolved_by_account_id_accounts_id_fk", + "tableFrom": "system_incidents", + "tableTo": "accounts", + "columnsFrom": [ + "resolved_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_settings": { + "name": "system_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "'global'" + }, + "registration_enabled": { + "name": "registration_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "oidc_enabled": { + "name": "oidc_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_issuer_url": { + "name": "oidc_issuer_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_client_id": { + "name": "oidc_client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_client_secret": { + "name": "oidc_client_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_scopes": { + "name": "oidc_scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'openid profile email'" + }, + "oidc_redirect_uri": { + "name": "oidc_redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_avatar_claim": { + "name": "oidc_avatar_claim", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'picture'" + }, + "oidc_auto_provision": { + "name": "oidc_auto_provision", + "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": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vital_records": { + "name": "vital_records", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "elder_id": { + "name": "elder_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recorded_at": { + "name": "recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "vital_record_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "systolic_bp": { + "name": "systolic_bp", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "diastolic_bp": { + "name": "diastolic_bp", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "heart_rate": { + "name": "heart_rate", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "temperature_tenths": { + "name": "temperature_tenths", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "spo2": { + "name": "spo2", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "blood_glucose_tenths": { + "name": "blood_glucose_tenths", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "weight_tenths": { + "name": "weight_tenths", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_by_account_id": { + "name": "created_by_account_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": { + "vital_records_recent_lookup": { + "name": "vital_records_recent_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "elder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recorded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vital_records_organization_id_organizations_id_fk": { + "name": "vital_records_organization_id_organizations_id_fk", + "tableFrom": "vital_records", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vital_records_elder_id_elders_id_fk": { + "name": "vital_records_elder_id_elders_id_fk", + "tableFrom": "vital_records", + "tableTo": "elders", + "columnsFrom": [ + "elder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vital_records_created_by_account_id_accounts_id_fk": { + "name": "vital_records_created_by_account_id_accounts_id_fk", + "tableFrom": "vital_records", + "tableTo": "accounts", + "columnsFrom": [ + "created_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.account_status": { + "name": "account_status", + "schema": "public", + "values": [ + "active", + "disabled", + "pending" + ] + }, + "public.admission_status": { + "name": "admission_status", + "schema": "public", + "values": [ + "active", + "transferred", + "discharged" + ] + }, + "public.alert_rule_status": { + "name": "alert_rule_status", + "schema": "public", + "values": [ + "enabled", + "disabled" + ] + }, + "public.alert_rule_type": { + "name": "alert_rule_type", + "schema": "public", + "values": [ + "health", + "care", + "device", + "incident", + "admission", + "other" + ] + }, + "public.alert_trigger_status": { + "name": "alert_trigger_status", + "schema": "public", + "values": [ + "open", + "acknowledged", + "resolved", + "closed" + ] + }, + "public.audit_result": { + "name": "audit_result", + "schema": "public", + "values": [ + "success", + "denied", + "failure" + ] + }, + "public.bed_status": { + "name": "bed_status", + "schema": "public", + "values": [ + "available", + "occupied", + "maintenance", + "disabled" + ] + }, + "public.care_level": { + "name": "care_level", + "schema": "public", + "values": [ + "self-care", + "semi-assisted", + "assisted", + "intensive" + ] + }, + "public.care_task_priority": { + "name": "care_task_priority", + "schema": "public", + "values": [ + "low", + "normal", + "high", + "urgent" + ] + }, + "public.care_task_status": { + "name": "care_task_status", + "schema": "public", + "values": [ + "pending", + "in_progress", + "completed", + "cancelled" + ] + }, + "public.care_task_type": { + "name": "care_task_type", + "schema": "public", + "values": [ + "daily_care", + "meal", + "medication", + "rehab", + "inspection", + "cleaning", + "other" + ] + }, + "public.chronic_condition_status": { + "name": "chronic_condition_status", + "schema": "public", + "values": [ + "active", + "controlled", + "resolved" + ] + }, + "public.device_asset_status": { + "name": "device_asset_status", + "schema": "public", + "values": [ + "active", + "maintenance", + "disabled", + "retired" + ] + }, + "public.elder_status": { + "name": "elder_status", + "schema": "public", + "values": [ + "active", + "pending", + "discharged" + ] + }, + "public.family_contact_status": { + "name": "family_contact_status", + "schema": "public", + "values": [ + "active", + "suspended", + "archived" + ] + }, + "public.family_feedback_status": { + "name": "family_feedback_status", + "schema": "public", + "values": [ + "open", + "in_progress", + "resolved", + "closed" + ] + }, + "public.family_feedback_type": { + "name": "family_feedback_type", + "schema": "public", + "values": [ + "service", + "care", + "meal", + "environment", + "other" + ] + }, + "public.family_visit_status": { + "name": "family_visit_status", + "schema": "public", + "values": [ + "requested", + "approved", + "completed", + "cancelled" + ] + }, + "public.gender": { + "name": "gender", + "schema": "public", + "values": [ + "male", + "female", + "other" + ] + }, + "public.health_review_status": { + "name": "health_review_status", + "schema": "public", + "values": [ + "pending", + "reviewed", + "resolved" + ] + }, + "public.incident_severity": { + "name": "incident_severity", + "schema": "public", + "values": [ + "info", + "warning", + "critical" + ] + }, + "public.incident_status": { + "name": "incident_status", + "schema": "public", + "values": [ + "open", + "acknowledged", + "resolved", + "closed" + ] + }, + "public.join_request_status": { + "name": "join_request_status", + "schema": "public", + "values": [ + "pending", + "approved", + "rejected" + ] + }, + "public.maintenance_ticket_priority": { + "name": "maintenance_ticket_priority", + "schema": "public", + "values": [ + "low", + "normal", + "high", + "urgent" + ] + }, + "public.maintenance_ticket_status": { + "name": "maintenance_ticket_status", + "schema": "public", + "values": [ + "open", + "assigned", + "resolved", + "closed", + "cancelled" + ] + }, + "public.membership_status": { + "name": "membership_status", + "schema": "public", + "values": [ + "active", + "disabled", + "pending" + ] + }, + "public.notice_status": { + "name": "notice_status", + "schema": "public", + "values": [ + "draft", + "published", + "retracted" + ] + }, + "public.organization_status": { + "name": "organization_status", + "schema": "public", + "values": [ + "active", + "disabled" + ] + }, + "public.role_scope": { + "name": "role_scope", + "schema": "public", + "values": [ + "platform", + "organization" + ] + }, + "public.vital_record_source": { + "name": "vital_record_source", + "schema": "public", + "values": [ + "manual", + "device", + "import" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index c204394..52c2fcb 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -43,6 +43,13 @@ "when": 1783071837853, "tag": "0005_quiet_sandman", "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1783077936270, + "tag": "0006_petite_night_nurse", + "breakpoints": true } ] } \ No newline at end of file diff --git a/modules/core/invitation-limits.ts b/modules/core/invitation-limits.ts new file mode 100644 index 0000000..98e5d9c --- /dev/null +++ b/modules/core/invitation-limits.ts @@ -0,0 +1,7 @@ +export const DEFAULT_INVITATION_VALIDITY_DAYS = 7; +export const MIN_INVITATION_VALIDITY_DAYS = 1; +export const MAX_INVITATION_VALIDITY_DAYS = 365; + +export const DEFAULT_INVITATION_MAX_USES = 1; +export const MIN_INVITATION_MAX_USES = 1; +export const MAX_INVITATION_MAX_USES = 100; diff --git a/modules/core/server/auth.ts b/modules/core/server/auth.ts index 7f40dcd..08e71b7 100644 --- a/modules/core/server/auth.ts +++ b/modules/core/server/auth.ts @@ -1,6 +1,6 @@ import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto"; -import { and, eq, gt, isNull } from "drizzle-orm"; +import { and, eq, gt, gte, isNull, lt, sql } from "drizzle-orm"; import { cookies } from "next/headers"; import { jsonFailure } from "@/modules/core/server/api"; @@ -324,12 +324,21 @@ export async function createRegistration(input: { .limit(1) : []; const invitation = invitationRows[0]; + const now = new Date(); if (input.invitationToken && !invitation) { throw new Error("邀请链接无效"); } - if (invitation && (invitation.invitation.status !== "active" || invitation.invitation.expiresAt < new Date())) { + if ( + invitation && + (invitation.invitation.status !== "active" || + invitation.invitation.expiresAt < now || + invitation.invitation.usedCount >= invitation.invitation.maxUses) + ) { throw new Error("邀请链接已失效"); } + if (invitation?.invitation.email && normalizeEmail(invitation.invitation.email) !== normalizedEmail) { + throw new Error("邀请邮箱与注册邮箱不一致"); + } if (!invitation && !settings.registrationEnabled) { throw new Error("系统暂未开放注册"); } @@ -372,15 +381,40 @@ export async function createRegistration(input: { roleId: invitation.invitation.roleId, status: "active", }); - await transaction + const consumedRows = await transaction .update(organizationInvitations) .set({ - status: "accepted", - acceptedByAccountId: account.id, - acceptedAt: new Date(), + usedCount: sql`${organizationInvitations.usedCount} + 1`, updatedAt: new Date(), }) - .where(eq(organizationInvitations.id, invitation.invitation.id)); + .where( + and( + eq(organizationInvitations.id, invitation.invitation.id), + eq(organizationInvitations.status, "active"), + gte(organizationInvitations.expiresAt, now), + lt(organizationInvitations.usedCount, organizationInvitations.maxUses), + ), + ) + .returning({ + id: organizationInvitations.id, + maxUses: organizationInvitations.maxUses, + usedCount: organizationInvitations.usedCount, + }); + const consumed = consumedRows[0]; + if (!consumed) { + throw new Error("邀请链接已失效"); + } + if (consumed.usedCount >= consumed.maxUses) { + await transaction + .update(organizationInvitations) + .set({ + status: "accepted", + acceptedByAccountId: account.id, + acceptedAt: new Date(), + updatedAt: new Date(), + }) + .where(eq(organizationInvitations.id, consumed.id)); + } } const sessionRows = await transaction diff --git a/modules/core/server/schema.ts b/modules/core/server/schema.ts index 42e9169..aa89f19 100644 --- a/modules/core/server/schema.ts +++ b/modules/core/server/schema.ts @@ -168,6 +168,8 @@ export const organizationInvitations = pgTable("organization_invitations", { acceptedByAccountId: uuid("accepted_by_account_id").references(() => accounts.id), acceptedAt: timestamp("accepted_at", { withTimezone: true }), expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), + maxUses: integer("max_uses").notNull().default(1), + usedCount: integer("used_count").notNull().default(0), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), }, (table) => ({ diff --git a/modules/core/server/settings.ts b/modules/core/server/settings.ts index c177de1..6ecfe4a 100644 --- a/modules/core/server/settings.ts +++ b/modules/core/server/settings.ts @@ -113,6 +113,8 @@ function toOrganizationInvitation(row: { acceptedByAccountId: row.invitation.acceptedByAccountId ?? undefined, acceptedAt: row.invitation.acceptedAt ? iso(row.invitation.acceptedAt) : undefined, expiresAt: iso(row.invitation.expiresAt), + maxUses: row.invitation.maxUses, + usedCount: row.invitation.usedCount, createdAt: iso(row.invitation.createdAt), updatedAt: iso(row.invitation.updatedAt), }; diff --git a/modules/core/server/store.ts b/modules/core/server/store.ts index b552665..b48c12e 100644 --- a/modules/core/server/store.ts +++ b/modules/core/server/store.ts @@ -210,6 +210,8 @@ export async function readData(): Promise { acceptedByAccountId: row.invitation.acceptedByAccountId ?? undefined, acceptedAt: row.invitation.acceptedAt ? iso(row.invitation.acceptedAt) : undefined, expiresAt: iso(row.invitation.expiresAt), + maxUses: row.invitation.maxUses, + usedCount: row.invitation.usedCount, createdAt: iso(row.invitation.createdAt), updatedAt: iso(row.invitation.updatedAt), })); diff --git a/modules/core/types.ts b/modules/core/types.ts index 51f6556..ea01c92 100644 --- a/modules/core/types.ts +++ b/modules/core/types.ts @@ -156,6 +156,8 @@ export type OrganizationInvitation = { acceptedByAccountId?: string; acceptedAt?: string; expiresAt: string; + maxUses: number; + usedCount: number; createdAt: string; updatedAt: string; }; diff --git a/modules/settings/components/GlobalSettingsClient.tsx b/modules/settings/components/GlobalSettingsClient.tsx index deaf8b9..1dce08a 100644 --- a/modules/settings/components/GlobalSettingsClient.tsx +++ b/modules/settings/components/GlobalSettingsClient.tsx @@ -167,7 +167,7 @@ export function GlobalSettingsClient({

机构邀请

- 邀请链接格式为 `/register?invite=...`,可限定邮箱并指定入组角色,适合定向开通账号。 + 邀请链接格式为 `/register?invite=...`,可限定邮箱、角色、有效期和使用次数,适合定向开通账号。

@@ -183,7 +183,7 @@ export function GlobalSettingsClient({

前往机构管理生成邀请码

-

选择机构行内的“邀请”,生成一次性或邮箱限定的注册链接。

+

选择机构行内的“邀请”,生成有有效期和使用次数限制的注册链接。