data holders: private tracker of every organisation holding my address, email, phone or IBAN
Some checks failed
Deploy production / deploy (push) Has been cancelled
Some checks failed
Deploy production / deploy (push) Has been cancelled
dataholders schema (orgs + identity) with migration and personal_app grants, idempotent CSV import (upsert on name+login, never wipes, keeps site edits and ticks), owner-only /trackers/data-holders with search, filters, pagination, inline edit/add and a moving checklist per identifier
This commit is contained in:
79
src/lib/server/db/schema/dataholders.ts
Normal file
79
src/lib/server/db/schema/dataholders.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { boolean, check, date, index, integer, text, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { dataholdersSchema } from './schemas';
|
||||
import { timestamps, timestamptz } from './helpers';
|
||||
|
||||
// Private: every organisation that holds data about Stan (owner only, see requireOwner).
|
||||
// Whenever the address, email, phone or IBAN changes, set the new value in
|
||||
// dataholders.identity and work down the orgs that still have the old one (the
|
||||
// /trackers/data-holders checklist). Never stores passwords: `login` is a username or email.
|
||||
|
||||
export const dataholderOrgs = dataholdersSchema.table(
|
||||
'orgs',
|
||||
{
|
||||
id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
|
||||
name: text('name').notNull(),
|
||||
category: text('category').notNull().default('Other'),
|
||||
/** Username or email used to log in. Never a password. */
|
||||
login: text('login'),
|
||||
/** Where the row came from, e.g. "Daisy checklist", "1Password". */
|
||||
source: text('source'),
|
||||
/** Websites/domains of the account (from 1Password). */
|
||||
domains: text('domains'),
|
||||
/** Stable import key (normalised name + login). Set by the importer, never by edits. */
|
||||
importKey: text('import_key'),
|
||||
holdsAddress: boolean('holds_address').notNull().default(false),
|
||||
holdsEmail: boolean('holds_email').notNull().default(false),
|
||||
holdsPhone: boolean('holds_phone').notNull().default(false),
|
||||
holdsIban: boolean('holds_iban').notNull().default(false),
|
||||
holdsBsn: boolean('holds_bsn').notNull().default(false),
|
||||
holdsCard: boolean('holds_card').notNull().default(false),
|
||||
holdsDob: boolean('holds_dob').notNull().default(false),
|
||||
holdsIdDoc: boolean('holds_id_doc').notNull().default(false),
|
||||
/** Free-text description of everything they hold (the "other" bucket). */
|
||||
dataHeld: text('data_held'),
|
||||
/** The address they have, as last seen (often an old one). */
|
||||
addressOnFile: text('address_on_file'),
|
||||
addressStatus: text('address_status').notNull().default('unknown'),
|
||||
/** Gets the address from the BRP automatically (government bodies). */
|
||||
followsBrp: boolean('follows_brp').notNull().default(false),
|
||||
emailOnFile: text('email_on_file'),
|
||||
phoneOnFile: text('phone_on_file'),
|
||||
action: text('action').notNull().default('update'),
|
||||
priority: text('priority').notNull().default('should'),
|
||||
howToChange: text('how_to_change'),
|
||||
link: text('link'),
|
||||
evidence: text('evidence'),
|
||||
notes: text('notes'),
|
||||
addressUpdatedOn: date('address_updated_on', { mode: 'string' }),
|
||||
emailUpdatedOn: date('email_updated_on', { mode: 'string' }),
|
||||
phoneUpdatedOn: date('phone_updated_on', { mode: 'string' }),
|
||||
ibanUpdatedOn: date('iban_updated_on', { mode: 'string' }),
|
||||
lastChecked: date('last_checked', { mode: 'string' }),
|
||||
/** Closed accounts stay in the list (archived), so their history is kept. */
|
||||
archived: boolean('archived').notNull().default(false),
|
||||
/** Set on any edit from the site; the importer then only fills empty fields. */
|
||||
editedAt: timestamptz('edited_at'),
|
||||
...timestamps
|
||||
},
|
||||
(t) => [
|
||||
uniqueIndex('orgs_import_key_key').on(t.importKey),
|
||||
index('orgs_category_idx').on(t.category),
|
||||
check('orgs_name_check', sql`length(btrim(${t.name})) > 0`),
|
||||
check('orgs_address_status_check', sql`${t.addressStatus} IN ('confirmed','likely','unknown')`),
|
||||
check('orgs_action_check', sql`${t.action} IN ('update','close','check','none')`),
|
||||
check('orgs_priority_check', sql`${t.priority} IN ('must','should','nice')`)
|
||||
]
|
||||
);
|
||||
|
||||
/** The current value of each identifier, and since when. Orgs ticked before `since` are due. */
|
||||
export const dataholderIdentity = dataholdersSchema.table(
|
||||
'identity',
|
||||
{
|
||||
kind: text('kind').primaryKey(),
|
||||
value: text('value').notNull(),
|
||||
since: date('since', { mode: 'string' }).notNull(),
|
||||
...timestamps
|
||||
},
|
||||
(t) => [check('identity_kind_check', sql`${t.kind} IN ('address','email','phone','iban')`)]
|
||||
);
|
||||
@@ -25,3 +25,4 @@ export * from './wildlife';
|
||||
export * from './trains';
|
||||
export * from './music';
|
||||
export * from './hardware';
|
||||
export * from './dataholders';
|
||||
|
||||
@@ -23,3 +23,4 @@ export const hardwareSchema = pgSchema('hardware');
|
||||
export const planesSchema = pgSchema('planes');
|
||||
export const wildlifeSchema = pgSchema('wildlife');
|
||||
export const trainsSchema = pgSchema('trains');
|
||||
export const dataholdersSchema = pgSchema('dataholders');
|
||||
|
||||
Reference in New Issue
Block a user