{
  "dataSource": "/OpenForum/AddOn/MemberServices/DB/member-services-model.dbmv.json",
  "model": {
    "tables": [
      {
        "id": "member",
        "name": "member",
        "columns": [
          {
            "name": "id",
            "type": "smallint",
            "primaryKey": true,
            "description": "DEFAULT nextval(member_id_seq), starts at 1234"
          },
          {
            "name": "first_name",
            "type": "text"
          },
          {
            "name": "last_name",
            "type": "text"
          },
          {
            "name": "alias",
            "type": "text"
          },
          {
            "name": "email",
            "type": "text"
          },
          {
            "name": "password",
            "type": "text",
            "description": "Stored hash"
          },
          {
            "name": "created_date",
            "type": "numeric"
          },
          {
            "name": "registered_date",
            "type": "numeric"
          },
          {
            "name": "verified_date",
            "type": "numeric"
          },
          {
            "name": "last_password_reset",
            "type": "numeric"
          }
        ],
        "extensions": {
          "description": "Registered members. alias is the sign-in name.",
          "datasource": "open-forum SQL (MemberServices)",
          "color": "blue",
          "notes": [
            "Keys and links are logical only - create_tables.sql declares no PK/FK constraints."
          ]
        }
      },
      {
        "id": "member_service_description",
        "name": "member_service_description",
        "columns": [
          {
            "name": "service_id",
            "type": "smallint",
            "primaryKey": true
          },
          {
            "name": "service_name",
            "type": "text"
          },
          {
            "name": "service_home_page",
            "type": "text"
          }
        ],
        "extensions": {
          "description": "Catalogue of services a member can subscribe to.",
          "datasource": "open-forum SQL (MemberServices)",
          "color": "violet",
          "notes": [
            "Keys and links are logical only - create_tables.sql declares no PK/FK constraints."
          ]
        }
      },
      {
        "id": "member_service",
        "name": "member_service",
        "columns": [
          {
            "name": "member_id",
            "type": "smallint",
            "foreignKey": {
              "targetTable": "member",
              "targetColumn": "id"
            }
          },
          {
            "name": "service_id",
            "type": "smallint",
            "foreignKey": {
              "targetTable": "member_service_description",
              "targetColumn": "service_id"
            },
            "description": "Has DEFAULT nextval(service_id_seq) in create_tables.sql, but is used as a reference to member_service_description"
          },
          {
            "name": "service_alias",
            "type": "text"
          },
          {
            "name": "used_count",
            "type": "numeric"
          },
          {
            "name": "available_count",
            "type": "numeric"
          },
          {
            "name": "start_date",
            "type": "numeric"
          },
          {
            "name": "end_date",
            "type": "numeric"
          },
          {
            "name": "access_token",
            "type": "text"
          },
          {
            "name": "last_used_date",
            "type": "numeric"
          }
        ],
        "extensions": {
          "description": "A member's subscription to a service, with usage counts and access token.",
          "datasource": "open-forum SQL (MemberServices)",
          "color": "violet",
          "notes": [
            "Keys and links are logical only - create_tables.sql declares no PK/FK constraints."
          ]
        }
      },
      {
        "id": "member_group",
        "name": "member_group",
        "columns": [
          {
            "name": "group_id",
            "type": "numeric",
            "primaryKey": true,
            "description": "DEFAULT nextval(group_id_seq)"
          },
          {
            "name": "name",
            "type": "text"
          },
          {
            "name": "owner_id",
            "type": "numeric",
            "foreignKey": {
              "targetTable": "member",
              "targetColumn": "id"
            }
          },
          {
            "name": "join_code",
            "type": "text",
            "description": "Added at runtime by Member.sjs ALTER TABLE if missing"
          }
        ],
        "extensions": {
          "description": "Member groups (used e.g. for APM project sharing).",
          "datasource": "open-forum SQL (MemberServices)",
          "color": "emerald",
          "notes": [
            "Keys and links are logical only - create_tables.sql declares no PK/FK constraints."
          ]
        }
      },
      {
        "id": "group_membership",
        "name": "group_membership",
        "columns": [
          {
            "name": "group_id",
            "type": "numeric",
            "foreignKey": {
              "targetTable": "member_group",
              "targetColumn": "group_id"
            }
          },
          {
            "name": "member_id",
            "type": "numeric",
            "foreignKey": {
              "targetTable": "member",
              "targetColumn": "id"
            }
          }
        ],
        "extensions": {
          "description": "Members of a group.",
          "datasource": "open-forum SQL (MemberServices)",
          "color": "emerald",
          "notes": [
            "Keys and links are logical only - create_tables.sql declares no PK/FK constraints."
          ]
        }
      },
      {
        "id": "group_admin",
        "name": "group_admin",
        "columns": [
          {
            "name": "group_id",
            "type": "numeric",
            "foreignKey": {
              "targetTable": "member_group",
              "targetColumn": "group_id"
            }
          },
          {
            "name": "member_id",
            "type": "numeric",
            "foreignKey": {
              "targetTable": "member",
              "targetColumn": "id"
            }
          }
        ],
        "extensions": {
          "description": "Admins of a group.",
          "datasource": "open-forum SQL (MemberServices)",
          "color": "emerald",
          "notes": [
            "Keys and links are logical only - create_tables.sql declares no PK/FK constraints."
          ]
        }
      },
      {
        "id": "group_join_request",
        "name": "group_join_request",
        "columns": [
          {
            "name": "group_id",
            "type": "numeric",
            "foreignKey": {
              "targetTable": "member_group",
              "targetColumn": "group_id"
            }
          },
          {
            "name": "member_id",
            "type": "numeric",
            "foreignKey": {
              "targetTable": "member",
              "targetColumn": "id"
            }
          },
          {
            "name": "status",
            "type": "text"
          },
          {
            "name": "created_date",
            "type": "numeric"
          }
        ],
        "extensions": {
          "description": "Pending, approved or rejected requests to join a group.",
          "datasource": "open-forum SQL (MemberServices)",
          "color": "emerald",
          "notes": [
            "Keys and links are logical only - create_tables.sql declares no PK/FK constraints."
          ]
        }
      },
      {
        "id": "account",
        "name": "account",
        "columns": [
          {
            "name": "account_id",
            "type": "numeric",
            "primaryKey": true
          },
          {
            "name": "owner_id",
            "type": "numeric",
            "foreignKey": {
              "targetTable": "member",
              "targetColumn": "id"
            }
          },
          {
            "name": "name",
            "type": "text"
          },
          {
            "name": "balance",
            "type": "numeric"
          }
        ],
        "extensions": {
          "description": "Member credit accounts (e.g. RenSMART credits).",
          "datasource": "open-forum SQL (MemberServices)",
          "color": "amber",
          "notes": [
            "Keys and links are logical only - create_tables.sql declares no PK/FK constraints."
          ]
        }
      },
      {
        "id": "account_ledger",
        "name": "account_ledger",
        "columns": [
          {
            "name": "account_ledger_id",
            "type": "numeric",
            "primaryKey": true
          },
          {
            "name": "account_id",
            "type": "numeric",
            "foreignKey": {
              "targetTable": "account",
              "targetColumn": "account_id"
            }
          },
          {
            "name": "owner_id",
            "type": "numeric",
            "foreignKey": {
              "targetTable": "member",
              "targetColumn": "id"
            }
          },
          {
            "name": "amount",
            "type": "numeric"
          },
          {
            "name": "description",
            "type": "text"
          },
          {
            "name": "source",
            "type": "text"
          },
          {
            "name": "entry_date",
            "type": "numeric"
          },
          {
            "name": "entry_type",
            "type": "text"
          }
        ],
        "extensions": {
          "description": "Ledger of credits and debits against an account.",
          "datasource": "open-forum SQL (MemberServices)",
          "color": "amber",
          "notes": [
            "Keys and links are logical only - create_tables.sql declares no PK/FK constraints."
          ]
        }
      },
      {
        "id": "project_services",
        "name": "project_services",
        "columns": [
          {
            "name": "member_id",
            "type": "numeric",
            "foreignKey": {
              "targetTable": "member",
              "targetColumn": "id"
            }
          },
          {
            "name": "project_id",
            "type": "numeric",
            "description": "Project id from an external (RenSMART) projects table"
          },
          {
            "name": "service_id",
            "type": "numeric",
            "description": "10010 = forecast email"
          }
        ],
        "extensions": {
          "description": "Legacy: per-project services such as the forecast email (service_id 10010). Not in create_tables.sql; columns inferred from MemberServices.sjs.",
          "datasource": "open-forum SQL (MemberServices)",
          "color": "rose",
          "notes": [
            "Keys and links are logical only - create_tables.sql declares no PK/FK constraints.",
            "Column types are guessed - only the column names are known from the INSERT in private/MemberServices.sjs."
          ]
        }
      },
      {
        "id": "member_verification_code",
        "name": "member_verification_code",
        "columns": [
          {
            "name": "email",
            "type": "text",
            "primaryKey": true,
            "description": "Lower-cased email the code was sent to. At most one row per email; a new request replaces the previous code."
          },
          {
            "name": "code_hash",
            "type": "text",
            "description": "SHA-256 of email, code and the site secret. The code itself is never stored."
          },
          {
            "name": "first_name",
            "type": "text",
            "description": "Registration details held until the member row is created on completion."
          },
          {
            "name": "last_name",
            "type": "text"
          },
          {
            "name": "alias",
            "type": "text",
            "description": "Requested member name, checked again on completion."
          },
          {
            "name": "remote_ip",
            "type": "text",
            "description": "Requesting IP, for the per-IP send limit."
          },
          {
            "name": "created",
            "type": "numeric",
            "description": "Start of the current one-hour send window (ms)."
          },
          {
            "name": "expires",
            "type": "numeric",
            "description": "Code expiry, 15 minutes after it was sent (ms)."
          },
          {
            "name": "attempts",
            "type": "numeric",
            "description": "Failed verify attempts; the code is invalidated at 5."
          },
          {
            "name": "sent_count",
            "type": "numeric",
            "description": "Codes sent in the current send window."
          },
          {
            "name": "last_sent",
            "type": "numeric",
            "description": "When the last code was sent (ms), for the 60 second resend limit."
          },
          {
            "name": "used",
            "type": "numeric",
            "description": "1 once the code has been verified."
          },
          {
            "name": "verify_token_hash",
            "type": "text",
            "description": "SHA-256 of the one-time token issued on a successful verify; needed to set the password."
          },
          {
            "name": "verify_token_expires",
            "type": "numeric",
            "description": "Token expiry, 30 minutes after verify (ms)."
          }
        ],
        "extensions": {
          "description": "Pending registration verification codes (MEM-T-012). The member row is only created once the code is verified and a password set; the row is then deleted.",
          "datasource": "open-forum SQL (MemberServices)",
          "color": "blue",
          "notes": [
            "No link to member: the member does not exist until registration completes. Rows older than a day are cleaned up on each new code request."
          ]
        }
      }
    ],
    "relationships": []
  },
  "view": {
    "tables": [
      {
        "type": "DBModelTableView",
        "tableId": "member_group",
        "x": 40,
        "y": 40,
        "color": "emerald"
      },
      {
        "type": "DBModelTableView",
        "tableId": "group_membership",
        "x": 40,
        "y": 364,
        "color": "emerald"
      },
      {
        "type": "DBModelTableView",
        "tableId": "group_admin",
        "x": 40,
        "y": 644,
        "color": "emerald"
      },
      {
        "type": "DBModelTableView",
        "tableId": "group_join_request",
        "x": 40,
        "y": 924,
        "color": "emerald"
      },
      {
        "type": "DBModelTableView",
        "tableId": "member",
        "x": 500,
        "y": 40,
        "color": "blue"
      },
      {
        "type": "DBModelTableView",
        "tableId": "project_services",
        "x": 500,
        "y": 496,
        "color": "rose"
      },
      {
        "type": "DBModelTableView",
        "tableId": "member_service",
        "x": 960,
        "y": 40,
        "color": "violet"
      },
      {
        "type": "DBModelTableView",
        "tableId": "member_service_description",
        "x": 960,
        "y": 474,
        "color": "violet"
      },
      {
        "type": "DBModelTableView",
        "tableId": "account",
        "x": 1420,
        "y": 40,
        "color": "amber"
      },
      {
        "type": "DBModelTableView",
        "tableId": "account_ledger",
        "x": 1420,
        "y": 364,
        "color": "amber"
      },
      {
        "type": "DBModelTableView",
        "tableId": "member_verification_code",
        "x": 500,
        "y": 900,
        "color": "blue"
      }
    ]
  }
}
