@marlinjai/contacts-core 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/{chunk-FPZSYMBL.js → chunk-5LG5GEWU.js} +12 -2
- package/dist/chunk-5LG5GEWU.js.map +1 -0
- package/dist/{chunk-OHFPW5YM.js → chunk-B2NV5TFC.js} +32 -1
- package/dist/{chunk-OHFPW5YM.js.map → chunk-B2NV5TFC.js.map} +1 -1
- package/dist/import-cli.js +170 -5
- package/dist/import-cli.js.map +1 -1
- package/dist/index.cjs +222 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +78 -4
- package/dist/index.d.ts +78 -4
- package/dist/index.js +205 -4
- package/dist/index.js.map +1 -1
- package/dist/migrate-cli.js +1 -1
- package/dist/model.cjs +13 -1
- package/dist/model.cjs.map +1 -1
- package/dist/model.d.cts +21 -2
- package/dist/model.d.ts +21 -2
- package/dist/model.js +5 -1
- package/package.json +44 -15
- package/dist/chunk-FPZSYMBL.js.map +0 -1
package/dist/import-cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli/import.ts","../src/contacts.ts","../src/model.ts","../src/uuid.ts","../src/db.ts","../src/import.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { contactsFor } from '../contacts.js';\nimport { createContactsDb } from '../db.js';\nimport { IMPORT_COLUMNS, importContacts, parseCsv } from '../import.js';\n\n/**\n * `contacts-import --tenant <company id> --file <path.csv> [--dry-run]`\n *\n * Imports an existing customer list. The file is personal data: keep it\n * outside every repository. The report names rows by line number only.\n * Reads the connection string from CONTACTS_DATABASE_URL.\n */\nfunction arg(name: string): string | undefined {\n const i = process.argv.indexOf(`--${name}`);\n return i >= 0 ? process.argv[i + 1] : undefined;\n}\n\nasync function main(): Promise<void> {\n const tenant = arg('tenant');\n const file = arg('file');\n const dryRun = process.argv.includes('--dry-run');\n const url = process.env.CONTACTS_DATABASE_URL;\n if (!tenant || !file) {\n console.error('usage: contacts-import --tenant <company id> --file <path.csv> [--dry-run]');\n console.error(`columns: ${IMPORT_COLUMNS.join(', ')}`);\n process.exit(2);\n }\n if (!url) {\n console.error('[contacts-import] CONTACTS_DATABASE_URL is not set');\n process.exit(2);\n }\n const rows = parseCsv(await readFile(file, 'utf8'));\n const handle = createContactsDb(url, { applicationName: 'import', max: 1 });\n try {\n const report = await importContacts(contactsFor(handle, tenant), rows, { dryRun });\n console.log(`[contacts-import] ${dryRun ? 'dry run, nothing written' : 'done'}`);\n console.log(` rows: ${report.rows}`);\n console.log(` ${dryRun ? 'would create' : 'created'}: ${report.created}`);\n console.log(` ${dryRun ? 'would number' : 'numbered'}: ${report.numbered}`);\n console.log(` unchanged: ${report.unchanged}`);\n console.log(` next customer number: ${report.nextCustomerNumber}`);\n for (const e of report.errors) {\n console.log(` error in line ${e.row}: ${e.code}${e.field ? ` (${e.field})` : ''}`);\n }\n if (report.errors.length > 0) process.exitCode = 1;\n } finally {\n await handle.close();\n }\n}\n\nmain().catch((e: unknown) => {\n console.error(`[contacts-import] failed: ${e instanceof Error ? e.message : String(e)}`);\n process.exit(1);\n});\n","import { sql, type Selectable, type Transaction } from 'kysely';\nimport type { ContactsDb, ContactsTable, Database } from './db.js';\nimport {\n ContactError,\n contactIdentityKey,\n isContactId,\n normalizeContactInput,\n type Contact,\n type ContactInput,\n type ContactKind,\n type NormalizedContact,\n} from './model.js';\nimport { uuidv7 } from './uuid.js';\n\ntype Row = Selectable<ContactsTable>;\n\nfunction toContact(row: Row): Contact {\n return {\n id: row.id,\n kind: row.kind,\n name: row.name,\n companyOrRole: row.company_or_role,\n organizationId: row.organization_id,\n email: row.email,\n phone: row.phone,\n note: row.note,\n legalForm: row.legal_form,\n addressLine1: row.address_line1,\n addressLine2: row.address_line2,\n postalCode: row.postal_code,\n city: row.city,\n country: row.country,\n vatId: row.vat_id,\n customerNumber: row.customer_number,\n version: row.version,\n archived: row.archived_at !== null,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n };\n}\n\nfunction columns(data: NormalizedContact) {\n return {\n name: data.name,\n company_or_role: data.companyOrRole,\n organization_id: data.organizationId,\n email: data.email,\n phone: data.phone,\n note: data.note,\n legal_form: data.legalForm,\n address_line1: data.addressLine1,\n address_line2: data.addressLine2,\n postal_code: data.postalCode,\n city: data.city,\n country: data.country,\n vat_id: data.vatId,\n identity_key: contactIdentityKey(data.kind, data.name, data.companyOrRole),\n };\n}\n\n/** A unique-index violation on the identity key (Postgres error 23505). */\nfunction isIdentityConflict(e: unknown): boolean {\n const err = e as { code?: string; constraint_name?: string };\n return err?.code === '23505' && err.constraint_name === 'contacts_identity';\n}\n\nfunction isCustomerNumberConflict(e: unknown): boolean {\n const err = e as { code?: string; constraint_name?: string };\n return err?.code === '23505' && err.constraint_name === 'contacts_customer_number';\n}\n\nfunction escapeLike(text: string): string {\n return text.replace(/[\\\\%_]/g, (c) => `\\\\${c}`);\n}\n\nexport interface ListOptions {\n kind?: ContactKind;\n /** Archived contacts are left out unless asked for. */\n includeArchived?: boolean;\n /** Case-insensitive match on name, company or role, and email. */\n search?: string;\n limit?: number;\n}\n\nexport interface ContactExport {\n contact: Contact;\n /** For a person linked to an organization. */\n organization: Contact | null;\n /** For an organization: the persons linked to it. */\n members: Contact[];\n}\n\n/**\n * The contacts of ONE company. Every query this object makes carries that\n * company's id, so an id from another company behaves exactly like an id that\n * does not exist. There is deliberately no way to query without a company.\n */\nexport interface Contacts {\n readonly tenantId: string;\n /** Sorted by name, then company or role. */\n list(options?: ListOptions): Promise<Contact[]>;\n get(id: string): Promise<Contact | null>;\n /** The given ids that exist in THIS company (archived included). Unknown and foreign ids are simply absent. */\n getMany(ids: readonly string[]): Promise<Contact[]>;\n /** The contact that would make `input` a duplicate, if there is one. */\n findSame(input: Pick<ContactInput, 'kind' | 'name' | 'companyOrRole'>, exceptId?: string): Promise<Contact | null>;\n /**\n * Throws `duplicate` (carrying the existing contact) when the identity\n * already exists. `id` is for moving existing data in; leave it out otherwise.\n */\n create(input: ContactInput, options?: { id?: string }): Promise<Contact>;\n /**\n * Corrects a contact; the kind cannot change. With `expectedVersion`, a save\n * based on an older version throws `stale` (carrying the current record)\n * instead of overwriting someone else's change.\n */\n update(id: string, input: ContactInput, options?: { expectedVersion?: number }): Promise<Contact>;\n /** Hides the contact from pickers. Anything that already names it keeps it. */\n archive(id: string): Promise<Contact>;\n restore(id: string): Promise<Contact>;\n /**\n * Folds `loserId` into `winnerId` and deletes the loser: empty fields of the\n * winner are filled from the loser, persons linked to a losing organization\n * move to the winner, a customer number moves when only the loser has one.\n * The CALLER repoints its own rows from the loser's id to the winner's.\n */\n merge(loserId: string, winnerId: string): Promise<Contact>;\n /** Gives the contact the company's next customer number. Returns the existing one when it already has one. */\n assignCustomerNumber(id: string): Promise<Contact>;\n /**\n * Sets a specific, already issued number (importing existing customers) and\n * moves the counter past it. Throws `customer_number_conflict` when the\n * number belongs to someone else or the contact has a different one.\n */\n setCustomerNumber(id: string, customerNumber: number): Promise<Contact>;\n /** The number the next `assignCustomerNumber` would hand out. */\n peekNextCustomerNumber(): Promise<number>;\n /** Deletes one contact for good. Persons linked to an erased organization keep their record, unlinked. */\n erase(id: string): Promise<boolean>;\n /** Deletes every contact and the counter of this company. Safe to repeat. Returns the number of contacts removed. */\n eraseAll(): Promise<number>;\n /** Everything held about one contact. */\n exportContact(id: string): Promise<ContactExport | null>;\n}\n\nexport function contactsFor(handle: ContactsDb, tenantId: string): Contacts {\n if (typeof tenantId !== 'string' || tenantId.length < 1 || tenantId.length > 64) {\n throw new TypeError('contactsFor: tenantId must be a non-empty string of at most 64 characters');\n }\n const { db } = handle;\n\n const own = (trx: Transaction<Database> | typeof db = db) =>\n trx.selectFrom('contacts').selectAll().where('tenant_id', '=', tenantId);\n\n async function findByIdentity(kind: ContactKind, name: string, companyOrRole: string, exceptId?: string) {\n let q = own()\n .where('kind', '=', kind)\n .where('identity_key', '=', contactIdentityKey(kind, name, companyOrRole));\n if (exceptId) q = q.where('id', '!=', exceptId);\n const row = await q.executeTakeFirst();\n return row ? toContact(row) : null;\n }\n\n /** A person's organization link must point at an organization of this company. */\n async function assertOrganization(organizationId: string | null) {\n if (organizationId === null) return;\n const row = await own().where('id', '=', organizationId).executeTakeFirst();\n if (!row || row.kind !== 'organization') throw new ContactError('invalid_organization');\n }\n\n async function lockOwn(trx: Transaction<Database>, id: string): Promise<Row> {\n if (!isContactId(id)) throw new ContactError('not_found');\n const row = await own(trx).where('id', '=', id).forUpdate().executeTakeFirst();\n if (!row) throw new ContactError('not_found');\n return row;\n }\n\n async function setArchived(id: string, archived: boolean): Promise<Contact> {\n await handle.ready();\n return db.transaction().execute(async (trx) => {\n const row = await lockOwn(trx, id);\n if ((row.archived_at !== null) === archived) return toContact(row);\n const updated = await trx\n .updateTable('contacts')\n .set({ archived_at: archived ? new Date() : null, version: sql`version + 1`, updated_at: new Date() })\n .where('tenant_id', '=', tenantId)\n .where('id', '=', id)\n .returningAll()\n .executeTakeFirstOrThrow();\n return toContact(updated);\n });\n }\n\n return {\n tenantId,\n\n async list(options = {}) {\n await handle.ready();\n let q = own();\n if (options.kind) q = q.where('kind', '=', options.kind);\n if (!options.includeArchived) q = q.where('archived_at', 'is', null);\n const term = options.search?.trim();\n if (term) {\n const pattern = `%${escapeLike(term)}%`;\n q = q.where((eb) =>\n eb.or([\n eb('name', 'ilike', pattern),\n eb('company_or_role', 'ilike', pattern),\n eb('email', 'ilike', pattern),\n ]),\n );\n }\n q = q.orderBy('name', 'asc').orderBy('company_or_role', 'asc').orderBy('id', 'asc');\n if (options.limit !== undefined) q = q.limit(Math.max(1, Math.min(options.limit, 5000)));\n return (await q.execute()).map(toContact);\n },\n\n async get(id) {\n if (!isContactId(id)) return null;\n await handle.ready();\n const row = await own().where('id', '=', id).executeTakeFirst();\n return row ? toContact(row) : null;\n },\n\n async getMany(ids) {\n const unique = [...new Set(ids)].filter(isContactId);\n if (unique.length === 0) return [];\n await handle.ready();\n return (await own().where('id', 'in', unique).execute()).map(toContact);\n },\n\n async findSame(input, exceptId) {\n const data = normalizeContactInput({ kind: input.kind, name: input.name, companyOrRole: input.companyOrRole });\n await handle.ready();\n return findByIdentity(data.kind, data.name, data.companyOrRole, exceptId);\n },\n\n async create(input, options = {}) {\n const data = normalizeContactInput(input);\n const id = options.id ?? uuidv7();\n if (!isContactId(id)) throw new ContactError('invalid_field', { field: 'id' });\n await handle.ready();\n await assertOrganization(data.organizationId);\n const inserted = await db\n .insertInto('contacts')\n .values({ id, tenant_id: tenantId, kind: data.kind, ...columns(data) })\n .onConflict((oc) => oc.columns(['tenant_id', 'kind', 'identity_key']).doNothing())\n .returningAll()\n .executeTakeFirst();\n if (!inserted) {\n const existing = await findByIdentity(data.kind, data.name, data.companyOrRole);\n throw new ContactError('duplicate', { existing: existing ?? undefined });\n }\n return toContact(inserted);\n },\n\n async update(id, input, options = {}) {\n await handle.ready();\n let data: NormalizedContact | undefined;\n try {\n return await db.transaction().execute(async (trx) => {\n const row = await lockOwn(trx, id);\n if (input.kind !== undefined && input.kind !== row.kind) {\n throw new ContactError('invalid_field', { field: 'kind' });\n }\n if (options.expectedVersion !== undefined && options.expectedVersion !== row.version) {\n throw new ContactError('stale', { existing: toContact(row) });\n }\n data = normalizeContactInput({ ...input, kind: row.kind });\n if (data.organizationId !== null) {\n const org = await own(trx).where('id', '=', data.organizationId).executeTakeFirst();\n if (!org || org.kind !== 'organization') throw new ContactError('invalid_organization');\n }\n const updated = await trx\n .updateTable('contacts')\n .set({ ...columns(data), version: sql`version + 1`, updated_at: new Date() })\n .where('tenant_id', '=', tenantId)\n .where('id', '=', id)\n .returningAll()\n .executeTakeFirstOrThrow();\n return toContact(updated);\n });\n } catch (e) {\n if (isIdentityConflict(e) && data) {\n const existing = await findByIdentity(data.kind, data.name, data.companyOrRole, id);\n throw new ContactError('duplicate', { existing: existing ?? undefined });\n }\n throw e;\n }\n },\n\n archive: (id) => setArchived(id, true),\n restore: (id) => setArchived(id, false),\n\n async merge(loserId, winnerId) {\n if (loserId === winnerId) throw new ContactError('invalid_field', { field: 'winnerId' });\n await handle.ready();\n return db.transaction().execute(async (trx) => {\n // Lock in id order, so two opposite merges cannot deadlock.\n const [firstId, secondId] = loserId < winnerId ? [loserId, winnerId] : [winnerId, loserId];\n const first = await lockOwn(trx, firstId);\n const second = await lockOwn(trx, secondId);\n const loser = first.id === loserId ? first : second;\n const winner = first.id === winnerId ? first : second;\n if (loser.kind !== winner.kind) throw new ContactError('merge_kind_mismatch');\n if (\n loser.customer_number !== null &&\n winner.customer_number !== null &&\n loser.customer_number !== winner.customer_number\n ) {\n throw new ContactError('customer_number_conflict');\n }\n if (loser.kind === 'organization') {\n await trx\n .updateTable('contacts')\n .set({ organization_id: winner.id, version: sql`version + 1`, updated_at: new Date() })\n .where('tenant_id', '=', tenantId)\n .where('organization_id', '=', loser.id)\n .execute();\n }\n // The loser goes first: its customer number must be free before the winner takes it.\n await trx.deleteFrom('contacts').where('tenant_id', '=', tenantId).where('id', '=', loser.id).execute();\n const updated = await trx\n .updateTable('contacts')\n .set({\n organization_id: winner.organization_id ?? loser.organization_id,\n email: winner.email ?? loser.email,\n phone: winner.phone ?? loser.phone,\n note: winner.note ?? loser.note,\n legal_form: winner.legal_form ?? loser.legal_form,\n address_line1: winner.address_line1 ?? loser.address_line1,\n address_line2: winner.address_line2 ?? loser.address_line2,\n postal_code: winner.postal_code ?? loser.postal_code,\n city: winner.city ?? loser.city,\n country: winner.country ?? loser.country,\n vat_id: winner.vat_id ?? loser.vat_id,\n customer_number: winner.customer_number ?? loser.customer_number,\n // A merge of an archived and an active contact leaves an active one.\n archived_at: winner.archived_at !== null && loser.archived_at !== null ? winner.archived_at : null,\n version: sql`version + 1`,\n updated_at: new Date(),\n })\n .where('tenant_id', '=', tenantId)\n .where('id', '=', winner.id)\n .returningAll()\n .executeTakeFirstOrThrow();\n return toContact(updated);\n });\n },\n\n async assignCustomerNumber(id) {\n await handle.ready();\n return db.transaction().execute(async (trx) => {\n const row = await lockOwn(trx, id);\n if (row.customer_number !== null) return toContact(row);\n const taken = await sql<{ n: number }>`\n INSERT INTO tenant_counters (tenant_id, next_customer_number) VALUES (${tenantId}, 2)\n ON CONFLICT (tenant_id) DO UPDATE SET next_customer_number = tenant_counters.next_customer_number + 1\n RETURNING next_customer_number - 1 AS n`.execute(trx);\n const n = taken.rows[0]!.n;\n const updated = await trx\n .updateTable('contacts')\n .set({ customer_number: n, version: sql`version + 1`, updated_at: new Date() })\n .where('tenant_id', '=', tenantId)\n .where('id', '=', id)\n .returningAll()\n .executeTakeFirstOrThrow();\n return toContact(updated);\n });\n },\n\n async setCustomerNumber(id, customerNumber) {\n if (!Number.isInteger(customerNumber) || customerNumber < 1) {\n throw new ContactError('invalid_field', { field: 'customerNumber' });\n }\n await handle.ready();\n try {\n return await db.transaction().execute(async (trx) => {\n const row = await lockOwn(trx, id);\n if (row.customer_number !== null && row.customer_number !== customerNumber) {\n throw new ContactError('customer_number_conflict');\n }\n await sql`\n INSERT INTO tenant_counters (tenant_id, next_customer_number) VALUES (${tenantId}, ${customerNumber + 1})\n ON CONFLICT (tenant_id) DO UPDATE\n SET next_customer_number = GREATEST(tenant_counters.next_customer_number, ${customerNumber + 1})`.execute(trx);\n if (row.customer_number === customerNumber) return toContact(row);\n const updated = await trx\n .updateTable('contacts')\n .set({ customer_number: customerNumber, version: sql`version + 1`, updated_at: new Date() })\n .where('tenant_id', '=', tenantId)\n .where('id', '=', id)\n .returningAll()\n .executeTakeFirstOrThrow();\n return toContact(updated);\n });\n } catch (e) {\n if (isCustomerNumberConflict(e)) throw new ContactError('customer_number_conflict');\n throw e;\n }\n },\n\n async peekNextCustomerNumber() {\n await handle.ready();\n const row = await db\n .selectFrom('tenant_counters')\n .select('next_customer_number')\n .where('tenant_id', '=', tenantId)\n .executeTakeFirst();\n return row?.next_customer_number ?? 1;\n },\n\n async erase(id) {\n if (!isContactId(id)) return false;\n await handle.ready();\n const result = await db\n .deleteFrom('contacts')\n .where('tenant_id', '=', tenantId)\n .where('id', '=', id)\n .executeTakeFirst();\n return Number(result.numDeletedRows) > 0;\n },\n\n async eraseAll() {\n await handle.ready();\n return db.transaction().execute(async (trx) => {\n const result = await trx.deleteFrom('contacts').where('tenant_id', '=', tenantId).executeTakeFirst();\n await trx.deleteFrom('tenant_counters').where('tenant_id', '=', tenantId).execute();\n return Number(result.numDeletedRows);\n });\n },\n\n async exportContact(id) {\n if (!isContactId(id)) return null;\n await handle.ready();\n const row = await own().where('id', '=', id).executeTakeFirst();\n if (!row) return null;\n const organization = row.organization_id\n ? await own().where('id', '=', row.organization_id).executeTakeFirst()\n : undefined;\n const members =\n row.kind === 'organization'\n ? await own().where('organization_id', '=', row.id).orderBy('name', 'asc').execute()\n : [];\n return {\n contact: toContact(row),\n organization: organization ? toContact(organization) : null,\n members: members.map(toContact),\n };\n },\n };\n}\n","/**\n * The contact model: types, validation and the identity key.\n *\n * This file is pure (no database, no Node built-ins) and safe to import\n * anywhere, including browser code, through `@marlinjai/contacts-core/model`.\n */\n\nexport type ContactKind = 'person' | 'organization';\n\nexport interface Contact {\n /** Stable id. Never reused, never derived from the name. */\n id: string;\n kind: ContactKind;\n name: string;\n /** Persons only: company or role as printed next to the name. Empty string when unknown. */\n companyOrRole: string;\n /** Persons only: the organization record this person belongs to, when linked. */\n organizationId: string | null;\n email: string | null;\n phone: string | null;\n note: string | null;\n /** Organizations only. */\n legalForm: string | null;\n addressLine1: string | null;\n addressLine2: string | null;\n postalCode: string | null;\n city: string | null;\n /** Two-letter country code, upper case. */\n country: string | null;\n vatId: string | null;\n /** Unique per company once assigned; never reused. */\n customerNumber: number | null;\n /** Rises by one with every change; pass it back as `expectedVersion` to refuse a stale save. */\n version: number;\n archived: boolean;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface ContactInput {\n kind?: ContactKind;\n name: string;\n companyOrRole?: string | null;\n organizationId?: string | null;\n email?: string | null;\n phone?: string | null;\n note?: string | null;\n legalForm?: string | null;\n addressLine1?: string | null;\n addressLine2?: string | null;\n postalCode?: string | null;\n city?: string | null;\n country?: string | null;\n vatId?: string | null;\n}\n\nexport type ContactErrorCode =\n | 'invalid_name'\n | 'invalid_field'\n | 'too_long'\n | 'duplicate'\n | 'not_found'\n | 'stale'\n | 'invalid_organization'\n | 'customer_number_conflict'\n | 'merge_kind_mismatch';\n\nexport class ContactError extends Error {\n readonly code: ContactErrorCode;\n /** For `duplicate`: the contact that already carries this identity. For `stale`: the current record. */\n readonly existing?: Contact;\n /** For `invalid_field` and `too_long`: the offending field. */\n readonly field?: string;\n constructor(code: ContactErrorCode, options: { existing?: Contact; field?: string } = {}) {\n super(options.field ? `${code}: ${options.field}` : code);\n this.name = 'ContactError';\n this.code = code;\n this.existing = options.existing;\n this.field = options.field;\n }\n}\n\nexport const CONTACT_NAME_MAX = 120;\nexport const ORGANIZATION_NAME_MAX = 160;\nexport const CONTACT_COMPANY_MAX = 160;\nexport const CONTACT_NOTE_MAX = 500;\nexport const CONTACT_EMAIL_MAX = 254;\nconst SHORT_FIELD_MAX = 160;\n\nfunction collapse(text: string): string {\n return text.replace(/\\s+/g, ' ').trim();\n}\n\nfunction optional(value: string | null | undefined, field: string, max: number): string | null {\n const text = collapse(String(value ?? ''));\n if (!text) return null;\n if (text.length > max) throw new ContactError('too_long', { field });\n return text;\n}\n\nexport interface NormalizedContact {\n kind: ContactKind;\n name: string;\n companyOrRole: string;\n organizationId: string | null;\n email: string | null;\n phone: string | null;\n note: string | null;\n legalForm: string | null;\n addressLine1: string | null;\n addressLine2: string | null;\n postalCode: string | null;\n city: string | null;\n country: string | null;\n vatId: string | null;\n}\n\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\nexport function isContactId(value: unknown): value is string {\n return typeof value === 'string' && UUID.test(value);\n}\n\n/**\n * Trim and validate. Throws ContactError('invalid_name' | 'invalid_field' | 'too_long').\n * Fields that do not belong to the kind (a legal form on a person, a company\n * text on an organization) are refused, never silently dropped.\n */\nexport function normalizeContactInput(input: ContactInput): NormalizedContact {\n const kind: ContactKind = input.kind ?? 'person';\n if (kind !== 'person' && kind !== 'organization') throw new ContactError('invalid_field', { field: 'kind' });\n\n const name = collapse(String(input.name ?? ''));\n if (!name) throw new ContactError('invalid_name');\n if (name.length > (kind === 'person' ? CONTACT_NAME_MAX : ORGANIZATION_NAME_MAX)) {\n throw new ContactError('too_long', { field: 'name' });\n }\n\n const companyOrRole = collapse(String(input.companyOrRole ?? ''));\n if (companyOrRole.length > CONTACT_COMPANY_MAX) throw new ContactError('too_long', { field: 'companyOrRole' });\n const organizationId = input.organizationId ?? null;\n const legalForm = optional(input.legalForm, 'legalForm', SHORT_FIELD_MAX);\n\n if (kind === 'organization') {\n if (companyOrRole) throw new ContactError('invalid_field', { field: 'companyOrRole' });\n if (organizationId) throw new ContactError('invalid_field', { field: 'organizationId' });\n } else {\n if (legalForm) throw new ContactError('invalid_field', { field: 'legalForm' });\n if (organizationId !== null && !isContactId(organizationId)) {\n throw new ContactError('invalid_field', { field: 'organizationId' });\n }\n }\n\n const emailRaw = optional(input.email, 'email', CONTACT_EMAIL_MAX);\n const email = emailRaw === null ? null : emailRaw.toLowerCase();\n if (email !== null && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email)) {\n throw new ContactError('invalid_field', { field: 'email' });\n }\n\n const countryRaw = optional(input.country, 'country', SHORT_FIELD_MAX);\n const country = countryRaw === null ? null : countryRaw.toUpperCase();\n if (country !== null && !/^[A-Z]{2}$/.test(country)) throw new ContactError('invalid_field', { field: 'country' });\n\n const noteRaw = String(input.note ?? '').trim();\n if (noteRaw.length > CONTACT_NOTE_MAX) throw new ContactError('too_long', { field: 'note' });\n\n return {\n kind,\n name,\n companyOrRole,\n organizationId,\n email,\n phone: optional(input.phone, 'phone', 40),\n note: noteRaw || null,\n legalForm,\n addressLine1: optional(input.addressLine1, 'addressLine1', SHORT_FIELD_MAX),\n addressLine2: optional(input.addressLine2, 'addressLine2', SHORT_FIELD_MAX),\n postalCode: optional(input.postalCode, 'postalCode', 20),\n city: optional(input.city, 'city', SHORT_FIELD_MAX),\n country,\n vatId: optional(input.vatId, 'vatId', 40),\n };\n}\n\n/**\n * The key two contacts of one company and one kind share when they are \"the\n * same\" for the duplicate check: the name for an organization, name plus\n * company or role for a person. Case and repeated whitespace do not count.\n */\nexport function contactIdentityKey(kind: ContactKind, name: string, companyOrRole = ''): string {\n const n = collapse(name).toLocaleLowerCase('de-DE');\n if (kind === 'organization') return n;\n // Unit separator: cannot appear in collapsed text, and (unlike NUL) Postgres text can hold it.\n return `${n}\\u001f${collapse(companyOrRole).toLocaleLowerCase('de-DE')}`;\n}\n\n/** How a person is printed: \"Name (Company)\" or just \"Name\". */\nexport function formatGuest(name: string, company: string): string {\n return company ? `${name} (${company})` : name;\n}\n\n/** \"0025\" for 25. Padding is presentation; the stored number is an integer. */\nexport function formatCustomerNumber(n: number, width = 4): string {\n return String(n).padStart(width, '0');\n}\n","import { randomBytes } from 'node:crypto';\n\n/**\n * A version 7 universally unique identifier (RFC 9562): 48 bits of Unix\n * milliseconds, then random bits. Sorts by creation time and needs no central\n * counter, so any app can create a contact id on its own.\n */\nexport function uuidv7(now: number = Date.now()): string {\n const bytes = randomBytes(16);\n bytes[0] = Math.floor(now / 2 ** 40) & 0xff;\n bytes[1] = Math.floor(now / 2 ** 32) & 0xff;\n bytes[2] = Math.floor(now / 2 ** 24) & 0xff;\n bytes[3] = Math.floor(now / 2 ** 16) & 0xff;\n bytes[4] = Math.floor(now / 2 ** 8) & 0xff;\n bytes[5] = now & 0xff;\n bytes[6] = (bytes[6]! & 0x0f) | 0x70;\n bytes[8] = (bytes[8]! & 0x3f) | 0x80;\n const hex = bytes.toString('hex');\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n","import { Kysely, type ColumnType, type Generated } from 'kysely';\nimport { PostgresJSDialect } from 'kysely-postgres-js';\nimport postgres, { type Sql } from 'postgres';\nimport { ContactsLayoutError, missingMigrations } from './migrate.js';\nimport type { MigrationSource } from './migrations/index.js';\n\ntype Timestamp = ColumnType<Date, Date | string | undefined, Date | string>;\n\nexport interface ContactsTable {\n id: string;\n tenant_id: string;\n kind: 'person' | 'organization';\n name: string;\n company_or_role: Generated<string>;\n organization_id: string | null;\n email: string | null;\n phone: string | null;\n note: string | null;\n legal_form: string | null;\n address_line1: string | null;\n address_line2: string | null;\n postal_code: string | null;\n city: string | null;\n country: string | null;\n vat_id: string | null;\n customer_number: number | null;\n identity_key: string;\n version: Generated<number>;\n archived_at: ColumnType<Date | null, Date | string | null | undefined, Date | string | null>;\n created_at: Timestamp;\n updated_at: Timestamp;\n}\n\nexport interface TenantCountersTable {\n tenant_id: string;\n next_customer_number: Generated<number>;\n}\n\nexport interface Database {\n contacts: ContactsTable;\n tenant_counters: TenantCountersTable;\n}\n\n/**\n * One app's handle on the contacts database. Create it once per process.\n *\n * Nothing connects until the first query, so an app starts and runs while the\n * contacts database is unreachable; only the calls that need it fail.\n */\nexport interface ContactsDb {\n readonly sql: Sql;\n readonly db: Kysely<Database>;\n /**\n * Resolves once the database has every migration this build needs. Throws\n * ContactsLayoutError when it is behind. A database that is ahead is fine.\n * The positive answer is remembered; a failure is asked again next time.\n */\n ready(): Promise<void>;\n close(): Promise<void>;\n}\n\nexport interface ContactsDbOptions {\n /** Shown in the database's connection list and logs, e.g. \"receipts\". Required: it is how a write is attributed. */\n applicationName: string;\n /** Connections this process may hold. Keep it small: many apps share one small instance. Default 3. */\n max?: number;\n /** Seconds before an idle connection is closed. Default 20. */\n idleTimeoutSeconds?: number;\n /** Seconds to wait for a connection before failing. Default 10. */\n connectTimeoutSeconds?: number;\n /** Tests only: the migrations `ready()` checks for. */\n migrations?: readonly MigrationSource[];\n}\n\nexport function createContactsDb(connectionString: string, options: ContactsDbOptions): ContactsDb {\n if (!connectionString) throw new TypeError('createContactsDb: a connection string is required');\n if (!options?.applicationName) throw new TypeError('createContactsDb: applicationName is required');\n const sql = postgres(connectionString, {\n max: options.max ?? 3,\n idle_timeout: options.idleTimeoutSeconds ?? 20,\n connect_timeout: options.connectTimeoutSeconds ?? 10,\n connection: { application_name: `contacts:${options.applicationName}`.slice(0, 63) },\n onnotice: () => {},\n });\n const db = new Kysely<Database>({ dialect: new PostgresJSDialect({ postgres: sql }) });\n\n let checked: Promise<void> | null = null;\n const ready = (): Promise<void> => {\n if (!checked) {\n const attempt = missingMigrations(sql, options.migrations).then((missing) => {\n if (missing.length > 0) throw new ContactsLayoutError(missing);\n });\n checked = attempt;\n // A failure (database down, layout behind) is not remembered.\n attempt.catch(() => {\n if (checked === attempt) checked = null;\n });\n }\n return checked;\n };\n\n return {\n sql,\n db,\n ready,\n close: async () => {\n await db.destroy();\n await sql.end({ timeout: 5 });\n },\n };\n}\n","import type { Contacts } from './contacts.js';\nimport { ContactError, normalizeContactInput, type ContactInput, type ContactKind } from './model.js';\n\n/**\n * Importing an existing customer list (for example the clients that already\n * carry a hand-assigned customer number).\n *\n * The file is personal data: it lives outside every repository, and nothing\n * here prints a name. Reports speak in row numbers and error codes only.\n */\n\nexport const IMPORT_COLUMNS = [\n 'customer_number',\n 'kind',\n 'name',\n 'company_or_role',\n 'legal_form',\n 'address_line1',\n 'address_line2',\n 'postal_code',\n 'city',\n 'country',\n 'vat_id',\n 'email',\n 'phone',\n 'note',\n] as const;\n\nexport type ImportRow = Partial<Record<(typeof IMPORT_COLUMNS)[number], string>>;\n\nexport class CsvError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'CsvError';\n }\n}\n\n/** A small RFC 4180 reader: comma separated, double quotes, doubled quote inside quotes, first line is the header. */\nexport function parseCsv(text: string): ImportRow[] {\n const records: string[][] = [];\n let field = '';\n let record: string[] = [];\n let quoted = false;\n const src = text.replace(/^/, '');\n for (let i = 0; i < src.length; i++) {\n const c = src[i]!;\n if (quoted) {\n if (c === '\"') {\n if (src[i + 1] === '\"') {\n field += '\"';\n i++;\n } else quoted = false;\n } else field += c;\n } else if (c === '\"') {\n if (field.length > 0) throw new CsvError(`line ${records.length + 1}: a quote inside an unquoted field`);\n quoted = true;\n } else if (c === ',') {\n record.push(field);\n field = '';\n } else if (c === '\\n' || c === '\\r') {\n if (c === '\\r' && src[i + 1] === '\\n') i++;\n record.push(field);\n field = '';\n records.push(record);\n record = [];\n } else field += c;\n }\n if (quoted) throw new CsvError('the file ends inside a quoted field');\n if (field.length > 0 || record.length > 0) {\n record.push(field);\n records.push(record);\n }\n const nonEmpty = records.filter((r) => r.some((f) => f.trim() !== ''));\n const header = nonEmpty.shift();\n if (!header) throw new CsvError('the file is empty');\n const names = header.map((h) => h.trim().toLowerCase());\n const allowed = new Set<string>(IMPORT_COLUMNS);\n for (const n of names) if (!allowed.has(n)) throw new CsvError(`unknown column \"${n}\"`);\n if (!names.includes('name')) throw new CsvError('the column \"name\" is required');\n if (new Set(names).size !== names.length) throw new CsvError('a column appears twice');\n return nonEmpty.map((r, index) => {\n if (r.length > names.length) throw new CsvError(`row ${index + 2}: more fields than columns`);\n const row: ImportRow = {};\n names.forEach((n, i) => {\n row[n as (typeof IMPORT_COLUMNS)[number]] = (r[i] ?? '').trim();\n });\n return row;\n });\n}\n\nexport type ImportOutcome = 'created' | 'numbered' | 'unchanged';\n\nexport interface ImportReport {\n rows: number;\n created: number;\n /** An existing contact that received its customer number. */\n numbered: number;\n unchanged: number;\n /** `row` is the line in the file (the header is line 1). */\n errors: { row: number; code: string; field?: string }[];\n nextCustomerNumber: number;\n}\n\nfunction toInput(row: ImportRow): { input: ContactInput; customerNumber: number | null } {\n const kindRaw = (row.kind ?? '').toLowerCase() || 'organization';\n if (kindRaw !== 'person' && kindRaw !== 'organization') throw new ContactError('invalid_field', { field: 'kind' });\n let customerNumber: number | null = null;\n if (row.customer_number) {\n if (!/^\\d{1,9}$/.test(row.customer_number)) throw new ContactError('invalid_field', { field: 'customer_number' });\n customerNumber = Number.parseInt(row.customer_number, 10);\n if (customerNumber < 1) throw new ContactError('invalid_field', { field: 'customer_number' });\n }\n return {\n customerNumber,\n input: {\n kind: kindRaw as ContactKind,\n name: row.name ?? '',\n companyOrRole: row.company_or_role,\n legalForm: row.legal_form,\n addressLine1: row.address_line1,\n addressLine2: row.address_line2,\n postalCode: row.postal_code,\n city: row.city,\n country: row.country,\n vatId: row.vat_id,\n email: row.email,\n phone: row.phone,\n note: row.note,\n },\n };\n}\n\n/**\n * Creates the contacts of `rows` and sets their customer numbers. Safe to run\n * again: a contact that exists (same kind, name and company) is not created a\n * second time and its fields are left alone; only a missing customer number is\n * set. With `dryRun` nothing is written and the report says what would happen.\n */\nexport async function importContacts(\n contacts: Contacts,\n rows: readonly ImportRow[],\n options: { dryRun?: boolean } = {},\n): Promise<ImportReport> {\n const report: ImportReport = {\n rows: rows.length,\n created: 0,\n numbered: 0,\n unchanged: 0,\n errors: [],\n nextCustomerNumber: 1,\n };\n const numbersInFile = new Map<number, number>();\n for (const [index, row] of rows.entries()) {\n const line = index + 2;\n try {\n const { input, customerNumber } = toInput(row);\n normalizeContactInput(input);\n if (customerNumber !== null) {\n const firstLine = numbersInFile.get(customerNumber);\n if (firstLine !== undefined) throw new ContactError('customer_number_conflict');\n numbersInFile.set(customerNumber, line);\n }\n const existing = await contacts.findSame(input);\n if (existing) {\n if (customerNumber === null || existing.customerNumber === customerNumber) {\n report.unchanged++;\n continue;\n }\n if (existing.customerNumber !== null) throw new ContactError('customer_number_conflict');\n if (!options.dryRun) await contacts.setCustomerNumber(existing.id, customerNumber);\n report.numbered++;\n continue;\n }\n if (!options.dryRun) {\n const created = await contacts.create(input);\n if (customerNumber !== null) {\n try {\n await contacts.setCustomerNumber(created.id, customerNumber);\n } catch (e) {\n // Do not leave a contact behind whose number could not be set.\n await contacts.erase(created.id);\n throw e;\n }\n }\n }\n report.created++;\n } catch (e) {\n if (e instanceof ContactError) report.errors.push({ row: line, code: e.code, field: e.field });\n else throw e;\n }\n }\n const stored = await contacts.peekNextCustomerNumber();\n const highestInFile = numbersInFile.size > 0 ? Math.max(...numbersInFile.keys()) : 0;\n report.nextCustomerNumber = options.dryRun ? Math.max(stored, highestInFile + 1) : stored;\n return report;\n}\n"],"mappings":"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","names":["sql"]}
|
|
1
|
+
{"version":3,"sources":["../src/cli/import.ts","../src/contacts.ts","../src/model.ts","../src/fields.ts","../src/uuid.ts","../src/db.ts","../src/import.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { contactsFor } from '../contacts.js';\nimport { createContactsDb } from '../db.js';\nimport { IMPORT_COLUMNS, importContacts, parseCsv } from '../import.js';\n\n/**\n * `contacts-import --tenant <company id> --file <path.csv> [--dry-run]`\n *\n * Imports an existing customer list. The file is personal data: keep it\n * outside every repository. The report names rows by line number only.\n * Reads the connection string from CONTACTS_DATABASE_URL.\n */\nfunction arg(name: string): string | undefined {\n const i = process.argv.indexOf(`--${name}`);\n return i >= 0 ? process.argv[i + 1] : undefined;\n}\n\nasync function main(): Promise<void> {\n const tenant = arg('tenant');\n const file = arg('file');\n const dryRun = process.argv.includes('--dry-run');\n const url = process.env.CONTACTS_DATABASE_URL;\n if (!tenant || !file) {\n console.error('usage: contacts-import --tenant <company id> --file <path.csv> [--dry-run]');\n console.error(`columns: ${IMPORT_COLUMNS.join(', ')}`);\n process.exit(2);\n }\n if (!url) {\n console.error('[contacts-import] CONTACTS_DATABASE_URL is not set');\n process.exit(2);\n }\n const rows = parseCsv(await readFile(file, 'utf8'));\n const handle = createContactsDb(url, { applicationName: 'import', max: 1 });\n try {\n const report = await importContacts(contactsFor(handle, tenant), rows, { dryRun });\n console.log(`[contacts-import] ${dryRun ? 'dry run, nothing written' : 'done'}`);\n console.log(` rows: ${report.rows}`);\n console.log(` ${dryRun ? 'would create' : 'created'}: ${report.created}`);\n console.log(` ${dryRun ? 'would number' : 'numbered'}: ${report.numbered}`);\n console.log(` unchanged: ${report.unchanged}`);\n console.log(` next customer number: ${report.nextCustomerNumber}`);\n for (const e of report.errors) {\n console.log(` error in line ${e.row}: ${e.code}${e.field ? ` (${e.field})` : ''}`);\n }\n if (report.errors.length > 0) process.exitCode = 1;\n } finally {\n await handle.close();\n }\n}\n\nmain().catch((e: unknown) => {\n console.error(`[contacts-import] failed: ${e instanceof Error ? e.message : String(e)}`);\n process.exit(1);\n});\n","import { sql, type Selectable, type Transaction } from 'kysely';\nimport { applyCustomFieldPatch, normalizeFieldDefinition, type FieldDefinition, type FieldDefinitionInput } from './fields.js';\nimport type { ContactsDb, ContactsTable, Database } from './db.js';\nimport {\n ContactError,\n contactIdentityKey,\n isContactId,\n normalizeContactInput,\n type Contact,\n type ContactInput,\n type ContactKind,\n type FieldValues,\n type NormalizedContact,\n type PreferredContact,\n} from './model.js';\nimport { uuidv7 } from './uuid.js';\n\ntype Row = Selectable<ContactsTable>;\n\nfunction toContact(row: Row): Contact {\n return {\n id: row.id,\n kind: row.kind,\n name: row.name,\n companyOrRole: row.company_or_role,\n organizationId: row.organization_id,\n email: row.email,\n phone: row.phone,\n note: row.note,\n legalForm: row.legal_form,\n addressLine1: row.address_line1,\n addressLine2: row.address_line2,\n postalCode: row.postal_code,\n city: row.city,\n country: row.country,\n vatId: row.vat_id,\n customerNumber: row.customer_number,\n preferredContact: row.preferred_contact as PreferredContact | null,\n customFields: (row.custom_fields ?? {}) as FieldValues,\n version: row.version,\n archived: row.archived_at !== null,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n };\n}\n\nfunction toDefinition(row: {\n key: string;\n label: string;\n type: string;\n options: string[] | null;\n archived_at: Date | null;\n created_at: Date;\n}): FieldDefinition {\n return {\n key: row.key,\n label: row.label,\n type: row.type as FieldDefinition['type'],\n options: row.options,\n archived: row.archived_at !== null,\n createdAt: row.created_at,\n };\n}\n\nfunction columns(data: NormalizedContact, customFields: FieldValues) {\n return {\n name: data.name,\n company_or_role: data.companyOrRole,\n organization_id: data.organizationId,\n email: data.email,\n phone: data.phone,\n note: data.note,\n legal_form: data.legalForm,\n address_line1: data.addressLine1,\n address_line2: data.addressLine2,\n postal_code: data.postalCode,\n city: data.city,\n country: data.country,\n vat_id: data.vatId,\n preferred_contact: data.preferredContact,\n custom_fields: sql`${JSON.stringify(customFields)}::text::jsonb`,\n identity_key: contactIdentityKey(data.kind, data.name, data.companyOrRole),\n };\n}\n\n/** A unique-index violation on the identity key (Postgres error 23505). */\nfunction isIdentityConflict(e: unknown): boolean {\n const err = e as { code?: string; constraint_name?: string };\n return err?.code === '23505' && err.constraint_name === 'contacts_identity';\n}\n\nfunction isCustomerNumberConflict(e: unknown): boolean {\n const err = e as { code?: string; constraint_name?: string };\n return err?.code === '23505' && err.constraint_name === 'contacts_customer_number';\n}\n\nfunction escapeLike(text: string): string {\n return text.replace(/[\\\\%_]/g, (c) => `\\\\${c}`);\n}\n\nexport interface ListOptions {\n kind?: ContactKind;\n /** Archived contacts are left out unless asked for. */\n includeArchived?: boolean;\n /** Case-insensitive match on name, company or role, and email. */\n search?: string;\n limit?: number;\n}\n\nexport interface ContactExport {\n contact: Contact;\n /** For a person linked to an organization. */\n organization: Contact | null;\n /** For an organization: the persons linked to it. */\n members: Contact[];\n}\n\n/**\n * The contacts of ONE company. Every query this object makes carries that\n * company's id, so an id from another company behaves exactly like an id that\n * does not exist. There is deliberately no way to query without a company.\n */\nexport interface Contacts {\n readonly tenantId: string;\n /** Sorted by name, then company or role. */\n list(options?: ListOptions): Promise<Contact[]>;\n get(id: string): Promise<Contact | null>;\n /** The given ids that exist in THIS company (archived included). Unknown and foreign ids are simply absent. */\n getMany(ids: readonly string[]): Promise<Contact[]>;\n /** The contact that would make `input` a duplicate, if there is one. */\n findSame(input: Pick<ContactInput, 'kind' | 'name' | 'companyOrRole'>, exceptId?: string): Promise<Contact | null>;\n /**\n * Throws `duplicate` (carrying the existing contact) when the identity\n * already exists. `id` is for moving existing data in; leave it out otherwise.\n */\n create(input: ContactInput, options?: { id?: string }): Promise<Contact>;\n /**\n * Corrects a contact; the kind cannot change. With `expectedVersion`, a save\n * based on an older version throws `stale` (carrying the current record)\n * instead of overwriting someone else's change.\n */\n update(id: string, input: ContactInput, options?: { expectedVersion?: number }): Promise<Contact>;\n /** Hides the contact from pickers. Anything that already names it keeps it. */\n archive(id: string): Promise<Contact>;\n restore(id: string): Promise<Contact>;\n /**\n * Folds `loserId` into `winnerId` and deletes the loser: empty fields of the\n * winner are filled from the loser, persons linked to a losing organization\n * move to the winner, a customer number moves when only the loser has one.\n * The CALLER repoints its own rows from the loser's id to the winner's.\n */\n merge(loserId: string, winnerId: string): Promise<Contact>;\n /** Gives the contact the company's next customer number. Returns the existing one when it already has one. */\n assignCustomerNumber(id: string): Promise<Contact>;\n /**\n * Sets a specific, already issued number (importing existing customers) and\n * moves the counter past it. Throws `customer_number_conflict` when the\n * number belongs to someone else or the contact has a different one.\n */\n setCustomerNumber(id: string, customerNumber: number): Promise<Contact>;\n /** The number the next `assignCustomerNumber` would hand out. */\n peekNextCustomerNumber(): Promise<number>;\n /** Defines a custom field for this company. Its key is unique per company; a key of an archived field stays taken. */\n createField(input: FieldDefinitionInput): Promise<FieldDefinition>;\n /** The company's custom fields. Archived ones are left out unless asked for. */\n listFields(options?: { includeArchived?: boolean }): Promise<FieldDefinition[]>;\n /** Archives a custom field by key. Stored values stay; new values are refused. */\n archiveField(key: string): Promise<FieldDefinition>;\n /** Deletes one contact for good. Persons linked to an erased organization keep their record, unlinked. */\n erase(id: string): Promise<boolean>;\n /** Deletes every contact and the counter of this company. Safe to repeat. Returns the number of contacts removed. */\n eraseAll(): Promise<number>;\n /** Everything held about one contact. */\n exportContact(id: string): Promise<ContactExport | null>;\n}\n\nexport function contactsFor(handle: ContactsDb, tenantId: string): Contacts {\n if (typeof tenantId !== 'string' || tenantId.length < 1 || tenantId.length > 64) {\n throw new TypeError('contactsFor: tenantId must be a non-empty string of at most 64 characters');\n }\n const { db } = handle;\n\n const own = (trx: Transaction<Database> | typeof db = db) =>\n trx.selectFrom('contacts').selectAll().where('tenant_id', '=', tenantId);\n\n /** Every definition of this company, archived ones included: their stored values need them. */\n async function loadDefinitions(trx: Transaction<Database> | typeof db = db): Promise<FieldDefinition[]> {\n const rows = await trx\n .selectFrom('contact_field_definitions')\n .selectAll()\n .where('tenant_id', '=', tenantId)\n .orderBy('created_at')\n .orderBy('key')\n .execute();\n return rows.map(toDefinition);\n }\n\n async function findByIdentity(kind: ContactKind, name: string, companyOrRole: string, exceptId?: string) {\n let q = own()\n .where('kind', '=', kind)\n .where('identity_key', '=', contactIdentityKey(kind, name, companyOrRole));\n if (exceptId) q = q.where('id', '!=', exceptId);\n const row = await q.executeTakeFirst();\n return row ? toContact(row) : null;\n }\n\n /** A person's organization link must point at an organization of this company. */\n async function assertOrganization(organizationId: string | null) {\n if (organizationId === null) return;\n const row = await own().where('id', '=', organizationId).executeTakeFirst();\n if (!row || row.kind !== 'organization') throw new ContactError('invalid_organization');\n }\n\n async function lockOwn(trx: Transaction<Database>, id: string): Promise<Row> {\n if (!isContactId(id)) throw new ContactError('not_found');\n const row = await own(trx).where('id', '=', id).forUpdate().executeTakeFirst();\n if (!row) throw new ContactError('not_found');\n return row;\n }\n\n async function setArchived(id: string, archived: boolean): Promise<Contact> {\n await handle.ready();\n return db.transaction().execute(async (trx) => {\n const row = await lockOwn(trx, id);\n if ((row.archived_at !== null) === archived) return toContact(row);\n const updated = await trx\n .updateTable('contacts')\n .set({ archived_at: archived ? new Date() : null, version: sql`version + 1`, updated_at: new Date() })\n .where('tenant_id', '=', tenantId)\n .where('id', '=', id)\n .returningAll()\n .executeTakeFirstOrThrow();\n return toContact(updated);\n });\n }\n\n return {\n tenantId,\n\n async list(options = {}) {\n await handle.ready();\n let q = own();\n if (options.kind) q = q.where('kind', '=', options.kind);\n if (!options.includeArchived) q = q.where('archived_at', 'is', null);\n const term = options.search?.trim();\n if (term) {\n const pattern = `%${escapeLike(term)}%`;\n q = q.where((eb) =>\n eb.or([\n eb('name', 'ilike', pattern),\n eb('company_or_role', 'ilike', pattern),\n eb('email', 'ilike', pattern),\n ]),\n );\n }\n q = q.orderBy('name', 'asc').orderBy('company_or_role', 'asc').orderBy('id', 'asc');\n if (options.limit !== undefined) q = q.limit(Math.max(1, Math.min(options.limit, 5000)));\n return (await q.execute()).map(toContact);\n },\n\n async get(id) {\n if (!isContactId(id)) return null;\n await handle.ready();\n const row = await own().where('id', '=', id).executeTakeFirst();\n return row ? toContact(row) : null;\n },\n\n async getMany(ids) {\n const unique = [...new Set(ids)].filter(isContactId);\n if (unique.length === 0) return [];\n await handle.ready();\n return (await own().where('id', 'in', unique).execute()).map(toContact);\n },\n\n async findSame(input, exceptId) {\n const data = normalizeContactInput({ kind: input.kind, name: input.name, companyOrRole: input.companyOrRole });\n await handle.ready();\n return findByIdentity(data.kind, data.name, data.companyOrRole, exceptId);\n },\n\n async create(input, options = {}) {\n const data = normalizeContactInput(input);\n const id = options.id ?? uuidv7();\n if (!isContactId(id)) throw new ContactError('invalid_field', { field: 'id' });\n await handle.ready();\n await assertOrganization(data.organizationId);\n const customFields = input.customFields\n ? applyCustomFieldPatch({}, input.customFields, await loadDefinitions())\n : {};\n const inserted = await db\n .insertInto('contacts')\n .values({ id, tenant_id: tenantId, kind: data.kind, ...columns(data, customFields) })\n .onConflict((oc) => oc.columns(['tenant_id', 'kind', 'identity_key']).doNothing())\n .returningAll()\n .executeTakeFirst();\n if (!inserted) {\n const existing = await findByIdentity(data.kind, data.name, data.companyOrRole);\n throw new ContactError('duplicate', { existing: existing ?? undefined });\n }\n return toContact(inserted);\n },\n\n async update(id, input, options = {}) {\n await handle.ready();\n let data: NormalizedContact | undefined;\n try {\n return await db.transaction().execute(async (trx) => {\n const row = await lockOwn(trx, id);\n if (input.kind !== undefined && input.kind !== row.kind) {\n throw new ContactError('invalid_field', { field: 'kind' });\n }\n if (options.expectedVersion !== undefined && options.expectedVersion !== row.version) {\n throw new ContactError('stale', { existing: toContact(row) });\n }\n data = normalizeContactInput({ ...input, kind: row.kind });\n if (data.organizationId !== null) {\n const org = await own(trx).where('id', '=', data.organizationId).executeTakeFirst();\n if (!org || org.kind !== 'organization') throw new ContactError('invalid_organization');\n }\n const currentFields = (row.custom_fields ?? {}) as FieldValues;\n const customFields = input.customFields\n ? applyCustomFieldPatch(currentFields, input.customFields, await loadDefinitions(trx))\n : currentFields;\n const updated = await trx\n .updateTable('contacts')\n .set({ ...columns(data, customFields), version: sql`version + 1`, updated_at: new Date() })\n .where('tenant_id', '=', tenantId)\n .where('id', '=', id)\n .returningAll()\n .executeTakeFirstOrThrow();\n return toContact(updated);\n });\n } catch (e) {\n if (isIdentityConflict(e) && data) {\n const existing = await findByIdentity(data.kind, data.name, data.companyOrRole, id);\n throw new ContactError('duplicate', { existing: existing ?? undefined });\n }\n throw e;\n }\n },\n\n async createField(input: FieldDefinitionInput) {\n await handle.ready();\n const data = normalizeFieldDefinition(input);\n try {\n const row = await db\n .insertInto('contact_field_definitions')\n .values({\n tenant_id: tenantId,\n id: uuidv7(),\n key: data.key,\n label: data.label,\n type: data.type,\n options: data.options === null ? null : sql`${JSON.stringify(data.options)}::text::jsonb`,\n })\n .returningAll()\n .executeTakeFirstOrThrow();\n return toDefinition(row);\n } catch (e) {\n if ((e as { code?: string; constraint_name?: string }).constraint_name === 'contact_field_definitions_key') {\n throw new ContactError('duplicate_field', { field: data.key });\n }\n throw e;\n }\n },\n\n async listFields(options = {}) {\n await handle.ready();\n const definitions = await loadDefinitions();\n return options.includeArchived ? definitions : definitions.filter((d) => !d.archived);\n },\n\n /** Archives a field: its stored values stay, and new values under it are refused. Repeating it changes nothing. */\n async archiveField(key) {\n await handle.ready();\n const row = await db\n .selectFrom('contact_field_definitions')\n .selectAll()\n .where('tenant_id', '=', tenantId)\n .where('key', '=', key)\n .executeTakeFirst();\n if (!row) throw new ContactError('field_not_found', { field: key });\n if (row.archived_at !== null) return toDefinition(row);\n const updated = await db\n .updateTable('contact_field_definitions')\n .set({ archived_at: new Date() })\n .where('tenant_id', '=', tenantId)\n .where('key', '=', key)\n .returningAll()\n .executeTakeFirstOrThrow();\n return toDefinition(updated);\n },\n\n archive: (id) => setArchived(id, true),\n restore: (id) => setArchived(id, false),\n\n async merge(loserId, winnerId) {\n if (loserId === winnerId) throw new ContactError('invalid_field', { field: 'winnerId' });\n await handle.ready();\n return db.transaction().execute(async (trx) => {\n // Lock in id order, so two opposite merges cannot deadlock.\n const [firstId, secondId] = loserId < winnerId ? [loserId, winnerId] : [winnerId, loserId];\n const first = await lockOwn(trx, firstId);\n const second = await lockOwn(trx, secondId);\n const loser = first.id === loserId ? first : second;\n const winner = first.id === winnerId ? first : second;\n if (loser.kind !== winner.kind) throw new ContactError('merge_kind_mismatch');\n if (\n loser.customer_number !== null &&\n winner.customer_number !== null &&\n loser.customer_number !== winner.customer_number\n ) {\n throw new ContactError('customer_number_conflict');\n }\n if (loser.kind === 'organization') {\n await trx\n .updateTable('contacts')\n .set({ organization_id: winner.id, version: sql`version + 1`, updated_at: new Date() })\n .where('tenant_id', '=', tenantId)\n .where('organization_id', '=', loser.id)\n .execute();\n }\n // The loser goes first: its customer number must be free before the winner takes it.\n await trx.deleteFrom('contacts').where('tenant_id', '=', tenantId).where('id', '=', loser.id).execute();\n const updated = await trx\n .updateTable('contacts')\n .set({\n organization_id: winner.organization_id ?? loser.organization_id,\n email: winner.email ?? loser.email,\n phone: winner.phone ?? loser.phone,\n note: winner.note ?? loser.note,\n legal_form: winner.legal_form ?? loser.legal_form,\n address_line1: winner.address_line1 ?? loser.address_line1,\n address_line2: winner.address_line2 ?? loser.address_line2,\n postal_code: winner.postal_code ?? loser.postal_code,\n city: winner.city ?? loser.city,\n country: winner.country ?? loser.country,\n vat_id: winner.vat_id ?? loser.vat_id,\n customer_number: winner.customer_number ?? loser.customer_number,\n // A merge of an archived and an active contact leaves an active one.\n archived_at: winner.archived_at !== null && loser.archived_at !== null ? winner.archived_at : null,\n version: sql`version + 1`,\n updated_at: new Date(),\n })\n .where('tenant_id', '=', tenantId)\n .where('id', '=', winner.id)\n .returningAll()\n .executeTakeFirstOrThrow();\n return toContact(updated);\n });\n },\n\n async assignCustomerNumber(id) {\n await handle.ready();\n return db.transaction().execute(async (trx) => {\n const row = await lockOwn(trx, id);\n if (row.customer_number !== null) return toContact(row);\n const taken = await sql<{ n: number }>`\n INSERT INTO tenant_counters (tenant_id, next_customer_number) VALUES (${tenantId}, 2)\n ON CONFLICT (tenant_id) DO UPDATE SET next_customer_number = tenant_counters.next_customer_number + 1\n RETURNING next_customer_number - 1 AS n`.execute(trx);\n const n = taken.rows[0]!.n;\n const updated = await trx\n .updateTable('contacts')\n .set({ customer_number: n, version: sql`version + 1`, updated_at: new Date() })\n .where('tenant_id', '=', tenantId)\n .where('id', '=', id)\n .returningAll()\n .executeTakeFirstOrThrow();\n return toContact(updated);\n });\n },\n\n async setCustomerNumber(id, customerNumber) {\n if (!Number.isInteger(customerNumber) || customerNumber < 1) {\n throw new ContactError('invalid_field', { field: 'customerNumber' });\n }\n await handle.ready();\n try {\n return await db.transaction().execute(async (trx) => {\n const row = await lockOwn(trx, id);\n if (row.customer_number !== null && row.customer_number !== customerNumber) {\n throw new ContactError('customer_number_conflict');\n }\n await sql`\n INSERT INTO tenant_counters (tenant_id, next_customer_number) VALUES (${tenantId}, ${customerNumber + 1})\n ON CONFLICT (tenant_id) DO UPDATE\n SET next_customer_number = GREATEST(tenant_counters.next_customer_number, ${customerNumber + 1})`.execute(trx);\n if (row.customer_number === customerNumber) return toContact(row);\n const updated = await trx\n .updateTable('contacts')\n .set({ customer_number: customerNumber, version: sql`version + 1`, updated_at: new Date() })\n .where('tenant_id', '=', tenantId)\n .where('id', '=', id)\n .returningAll()\n .executeTakeFirstOrThrow();\n return toContact(updated);\n });\n } catch (e) {\n if (isCustomerNumberConflict(e)) throw new ContactError('customer_number_conflict');\n throw e;\n }\n },\n\n async peekNextCustomerNumber() {\n await handle.ready();\n const row = await db\n .selectFrom('tenant_counters')\n .select('next_customer_number')\n .where('tenant_id', '=', tenantId)\n .executeTakeFirst();\n return row?.next_customer_number ?? 1;\n },\n\n async erase(id) {\n if (!isContactId(id)) return false;\n await handle.ready();\n const result = await db\n .deleteFrom('contacts')\n .where('tenant_id', '=', tenantId)\n .where('id', '=', id)\n .executeTakeFirst();\n return Number(result.numDeletedRows) > 0;\n },\n\n async eraseAll() {\n await handle.ready();\n return db.transaction().execute(async (trx) => {\n const result = await trx.deleteFrom('contacts').where('tenant_id', '=', tenantId).executeTakeFirst();\n await trx.deleteFrom('tenant_counters').where('tenant_id', '=', tenantId).execute();\n return Number(result.numDeletedRows);\n });\n },\n\n async exportContact(id) {\n if (!isContactId(id)) return null;\n await handle.ready();\n const row = await own().where('id', '=', id).executeTakeFirst();\n if (!row) return null;\n const organization = row.organization_id\n ? await own().where('id', '=', row.organization_id).executeTakeFirst()\n : undefined;\n const members =\n row.kind === 'organization'\n ? await own().where('organization_id', '=', row.id).orderBy('name', 'asc').execute()\n : [];\n return {\n contact: toContact(row),\n organization: organization ? toContact(organization) : null,\n members: members.map(toContact),\n };\n },\n };\n}\n","/**\n * The contact model: types, validation and the identity key.\n *\n * This file is pure (no database, no Node built-ins) and safe to import\n * anywhere, including browser code, through `@marlinjai/contacts-core/model`.\n */\n\nexport type ContactKind = 'person' | 'organization';\n\n/** How a contact prefers to be reached. Null when none was recorded. */\nexport const PREFERRED_CONTACT = ['email', 'phone', 'post', 'none'] as const;\nexport type PreferredContact = (typeof PREFERRED_CONTACT)[number];\n\n/** The field types of a custom field: the data table's column type names (see fields.ts). */\nexport const FIELD_TYPES = ['text', 'number', 'date', 'boolean', 'select', 'multi_select', 'url'] as const;\nexport type FieldType = (typeof FIELD_TYPES)[number];\n\n/** One stored value of a custom field. Shapes are checked by fields.ts against the field's type. */\nexport type FieldValue = string | number | boolean | string[];\nexport type FieldValues = Record<string, FieldValue>;\n\nexport interface Contact {\n /** Stable id. Never reused, never derived from the name. */\n id: string;\n kind: ContactKind;\n name: string;\n /** Persons only: company or role as printed next to the name. Empty string when unknown. */\n companyOrRole: string;\n /** Persons only: the organization record this person belongs to, when linked. */\n organizationId: string | null;\n email: string | null;\n phone: string | null;\n note: string | null;\n /** Organizations only. */\n legalForm: string | null;\n addressLine1: string | null;\n addressLine2: string | null;\n postalCode: string | null;\n city: string | null;\n /** Two-letter country code, upper case. */\n country: string | null;\n vatId: string | null;\n /** Unique per company once assigned; never reused. */\n customerNumber: number | null;\n preferredContact: PreferredContact | null;\n /** The company's custom fields set on this contact, keyed by field key. */\n customFields: FieldValues;\n /** Rises by one with every change; pass it back as `expectedVersion` to refuse a stale save. */\n version: number;\n archived: boolean;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface ContactInput {\n kind?: ContactKind;\n name: string;\n companyOrRole?: string | null;\n organizationId?: string | null;\n email?: string | null;\n phone?: string | null;\n note?: string | null;\n legalForm?: string | null;\n addressLine1?: string | null;\n addressLine2?: string | null;\n postalCode?: string | null;\n city?: string | null;\n country?: string | null;\n vatId?: string | null;\n preferredContact?: string | null;\n /**\n * Field values to set, keyed by field key. A null value removes that field's value.\n * Keys not given are left as they are. Checked against the company's definitions.\n */\n customFields?: Record<string, unknown>;\n}\n\nexport type ContactErrorCode =\n | 'invalid_name'\n | 'invalid_field'\n | 'too_long'\n | 'duplicate'\n | 'not_found'\n | 'stale'\n | 'invalid_organization'\n | 'customer_number_conflict'\n | 'merge_kind_mismatch'\n | 'unknown_field'\n | 'field_archived'\n | 'invalid_value'\n | 'duplicate_field'\n | 'field_not_found';\n\nexport class ContactError extends Error {\n readonly code: ContactErrorCode;\n /** For `duplicate`: the contact that already carries this identity. For `stale`: the current record. */\n readonly existing?: Contact;\n /** For `invalid_field` and `too_long`: the offending field. */\n readonly field?: string;\n constructor(code: ContactErrorCode, options: { existing?: Contact; field?: string } = {}) {\n super(options.field ? `${code}: ${options.field}` : code);\n this.name = 'ContactError';\n this.code = code;\n this.existing = options.existing;\n this.field = options.field;\n }\n}\n\nexport const CONTACT_NAME_MAX = 120;\nexport const ORGANIZATION_NAME_MAX = 160;\nexport const CONTACT_COMPANY_MAX = 160;\nexport const CONTACT_NOTE_MAX = 500;\nexport const CONTACT_EMAIL_MAX = 254;\nconst SHORT_FIELD_MAX = 160;\n\nfunction collapse(text: string): string {\n return text.replace(/\\s+/g, ' ').trim();\n}\n\nfunction optional(value: string | null | undefined, field: string, max: number): string | null {\n const text = collapse(String(value ?? ''));\n if (!text) return null;\n if (text.length > max) throw new ContactError('too_long', { field });\n return text;\n}\n\nexport interface NormalizedContact {\n kind: ContactKind;\n name: string;\n companyOrRole: string;\n organizationId: string | null;\n email: string | null;\n phone: string | null;\n note: string | null;\n legalForm: string | null;\n addressLine1: string | null;\n addressLine2: string | null;\n postalCode: string | null;\n city: string | null;\n country: string | null;\n vatId: string | null;\n preferredContact: PreferredContact | null;\n}\n\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\nexport function isContactId(value: unknown): value is string {\n return typeof value === 'string' && UUID.test(value);\n}\n\n/**\n * Trim and validate. Throws ContactError('invalid_name' | 'invalid_field' | 'too_long').\n * Fields that do not belong to the kind (a legal form on a person, a company\n * text on an organization) are refused, never silently dropped.\n */\nexport function normalizeContactInput(input: ContactInput): NormalizedContact {\n const kind: ContactKind = input.kind ?? 'person';\n if (kind !== 'person' && kind !== 'organization') throw new ContactError('invalid_field', { field: 'kind' });\n\n const name = collapse(String(input.name ?? ''));\n if (!name) throw new ContactError('invalid_name');\n if (name.length > (kind === 'person' ? CONTACT_NAME_MAX : ORGANIZATION_NAME_MAX)) {\n throw new ContactError('too_long', { field: 'name' });\n }\n\n const companyOrRole = collapse(String(input.companyOrRole ?? ''));\n if (companyOrRole.length > CONTACT_COMPANY_MAX) throw new ContactError('too_long', { field: 'companyOrRole' });\n const organizationId = input.organizationId ?? null;\n const legalForm = optional(input.legalForm, 'legalForm', SHORT_FIELD_MAX);\n\n if (kind === 'organization') {\n if (companyOrRole) throw new ContactError('invalid_field', { field: 'companyOrRole' });\n if (organizationId) throw new ContactError('invalid_field', { field: 'organizationId' });\n } else {\n if (legalForm) throw new ContactError('invalid_field', { field: 'legalForm' });\n if (organizationId !== null && !isContactId(organizationId)) {\n throw new ContactError('invalid_field', { field: 'organizationId' });\n }\n }\n\n const emailRaw = optional(input.email, 'email', CONTACT_EMAIL_MAX);\n const email = emailRaw === null ? null : emailRaw.toLowerCase();\n if (email !== null && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email)) {\n throw new ContactError('invalid_field', { field: 'email' });\n }\n\n const countryRaw = optional(input.country, 'country', SHORT_FIELD_MAX);\n const country = countryRaw === null ? null : countryRaw.toUpperCase();\n if (country !== null && !/^[A-Z]{2}$/.test(country)) throw new ContactError('invalid_field', { field: 'country' });\n\n const noteRaw = String(input.note ?? '').trim();\n if (noteRaw.length > CONTACT_NOTE_MAX) throw new ContactError('too_long', { field: 'note' });\n\n return {\n kind,\n name,\n companyOrRole,\n organizationId,\n email,\n phone: optional(input.phone, 'phone', 40),\n note: noteRaw || null,\n legalForm,\n addressLine1: optional(input.addressLine1, 'addressLine1', SHORT_FIELD_MAX),\n addressLine2: optional(input.addressLine2, 'addressLine2', SHORT_FIELD_MAX),\n postalCode: optional(input.postalCode, 'postalCode', 20),\n city: optional(input.city, 'city', SHORT_FIELD_MAX),\n country,\n vatId: optional(input.vatId, 'vatId', 40),\n preferredContact: preferredContact(input.preferredContact),\n };\n}\n\nfunction preferredContact(value: string | null | undefined): PreferredContact | null {\n if (value === null || value === undefined) return null;\n if ((PREFERRED_CONTACT as readonly string[]).includes(value)) return value as PreferredContact;\n throw new ContactError('invalid_field', { field: 'preferredContact' });\n}\n\n/**\n * The key two contacts of one company and one kind share when they are \"the\n * same\" for the duplicate check: the name for an organization, name plus\n * company or role for a person. Case and repeated whitespace do not count.\n */\nexport function contactIdentityKey(kind: ContactKind, name: string, companyOrRole = ''): string {\n const n = collapse(name).toLocaleLowerCase('de-DE');\n if (kind === 'organization') return n;\n // Unit separator: cannot appear in collapsed text, and (unlike NUL) Postgres text can hold it.\n return `${n}\\u001f${collapse(companyOrRole).toLocaleLowerCase('de-DE')}`;\n}\n\n/** How a person is printed: \"Name (Company)\" or just \"Name\". */\nexport function formatGuest(name: string, company: string): string {\n return company ? `${name} (${company})` : name;\n}\n\n/** \"0025\" for 25. Padding is presentation; the stored number is an integer. */\nexport function formatCustomerNumber(n: number, width = 4): string {\n return String(n).padStart(width, '0');\n}\n","import type { ColumnType } from '@marlinjai/data-table-core';\nimport { ContactError, FIELD_TYPES, type FieldType, type FieldValue, type FieldValues } from './model.js';\n\n/**\n * Custom field definitions and the checks that every stored value must pass.\n *\n * The type names are the data table's column type names (`ColumnType` in\n * @marlinjai/data-table-core), which export no value validation, so the rules\n * live here. A value is either accepted exactly as given, or refused with a\n * typed error that names the field. Nothing is coerced: a number is never read\n * from a string, and a date must be a real calendar day.\n */\n\nexport const FIELD_KEY = /^[a-z][a-z0-9_]{1,39}$/;\nexport const FIELD_LABEL_MAX = 80;\nexport const FIELD_OPTION_MAX = 80;\nexport const FIELD_OPTIONS_MAX = 100;\nexport const FIELD_TEXT_MAX = 2000;\nexport const FIELD_URL_MAX = 2048;\n\nexport interface FieldDefinition {\n key: string;\n label: string;\n type: FieldType;\n /** Choices for select and multi_select; null for every other type. */\n options: string[] | null;\n archived: boolean;\n createdAt: Date;\n}\n\nexport interface FieldDefinitionInput {\n key: string;\n label: string;\n type: string;\n options?: readonly string[] | null;\n}\n\nexport interface NormalizedFieldDefinition {\n key: string;\n label: string;\n type: FieldType;\n options: string[] | null;\n}\n\n// Compile-time guard: every field type must be a column type of the data table. Erased at run time.\nconst fieldTypesAreColumnTypes: readonly ColumnType[] = FIELD_TYPES;\nvoid fieldTypesAreColumnTypes;\n\nconst collapse = (text: string) => text.replace(/\\s+/g, ' ').trim();\n\n/** Validates a definition before it is stored. Throws ContactError('invalid_field' | 'too_long'). */\nexport function normalizeFieldDefinition(input: FieldDefinitionInput): NormalizedFieldDefinition {\n const key = String(input.key ?? '');\n if (!FIELD_KEY.test(key)) throw new ContactError('invalid_field', { field: 'key' });\n\n const label = collapse(String(input.label ?? ''));\n if (!label) throw new ContactError('invalid_field', { field: 'label' });\n if (label.length > FIELD_LABEL_MAX) throw new ContactError('too_long', { field: 'label' });\n\n if (!(FIELD_TYPES as readonly string[]).includes(input.type)) {\n throw new ContactError('invalid_field', { field: 'type' });\n }\n const type = input.type as FieldType;\n\n const choosable = type === 'select' || type === 'multi_select';\n if (!choosable) {\n if (input.options !== undefined && input.options !== null) {\n throw new ContactError('invalid_field', { field: 'options' });\n }\n return { key, label, type, options: null };\n }\n\n if (!Array.isArray(input.options) || input.options.length === 0 || input.options.length > FIELD_OPTIONS_MAX) {\n throw new ContactError('invalid_field', { field: 'options' });\n }\n const options = input.options.map((o) => collapse(String(o)));\n for (const option of options) {\n if (!option) throw new ContactError('invalid_field', { field: 'options' });\n if (option.length > FIELD_OPTION_MAX) throw new ContactError('too_long', { field: 'options' });\n }\n if (new Set(options).size !== options.length) throw new ContactError('invalid_field', { field: 'options' });\n return { key, label, type, options };\n}\n\nconst ISO_DAY = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\n\n/** Checks one value against its definition. Returns the value as stored. Throws ContactError. */\nexport function checkFieldValue(def: FieldDefinition, value: unknown): FieldValue {\n const refuse = () => new ContactError('invalid_value', { field: def.key });\n switch (def.type) {\n case 'text': {\n if (typeof value !== 'string' || value.length === 0 || value.length > FIELD_TEXT_MAX) throw refuse();\n if (value !== value.trim()) throw refuse();\n return value;\n }\n case 'number': {\n if (typeof value !== 'number' || !Number.isFinite(value)) throw refuse();\n return value;\n }\n case 'date': {\n if (typeof value !== 'string') throw refuse();\n const match = ISO_DAY.exec(value);\n if (!match) throw refuse();\n const [year, month, day] = [Number(match[1]), Number(match[2]), Number(match[3])];\n const date = new Date(Date.UTC(year, month - 1, day));\n if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) throw refuse();\n return value;\n }\n case 'boolean': {\n if (typeof value !== 'boolean') throw refuse();\n return value;\n }\n case 'select': {\n if (typeof value !== 'string' || !def.options?.includes(value)) throw refuse();\n return value;\n }\n case 'multi_select': {\n if (!Array.isArray(value) || value.length === 0) throw refuse();\n if (!value.every((v) => typeof v === 'string' && def.options?.includes(v))) throw refuse();\n if (new Set(value).size !== value.length) throw refuse();\n return [...value];\n }\n case 'url': {\n if (typeof value !== 'string' || value.length > FIELD_URL_MAX) throw refuse();\n let parsed: URL;\n try {\n parsed = new URL(value);\n } catch {\n throw refuse();\n }\n if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') throw refuse();\n return value;\n }\n }\n}\n\n/**\n * Applies a patch of custom field values to a contact's current values.\n *\n * - A key without a definition is refused (`unknown_field`), never dropped.\n * - A key whose definition is archived is refused (`field_archived`) when a value\n * is set. Clearing it (null) is allowed, so an archived field can be emptied.\n * - A null value removes the key. Keys not in the patch are left as they are,\n * including values under archived definitions.\n */\nexport function applyCustomFieldPatch(\n current: FieldValues,\n patch: Record<string, unknown>,\n definitions: readonly FieldDefinition[],\n): FieldValues {\n const byKey = new Map(definitions.map((d) => [d.key, d]));\n const next: FieldValues = { ...current };\n for (const [key, value] of Object.entries(patch)) {\n if (value === undefined) continue;\n const def = byKey.get(key);\n if (!def) throw new ContactError('unknown_field', { field: key });\n if (value === null) {\n delete next[key];\n continue;\n }\n if (def.archived) throw new ContactError('field_archived', { field: key });\n next[key] = checkFieldValue(def, value);\n }\n return next;\n}\n","import { randomBytes } from 'node:crypto';\n\n/**\n * A version 7 universally unique identifier (RFC 9562): 48 bits of Unix\n * milliseconds, then random bits. Sorts by creation time and needs no central\n * counter, so any app can create a contact id on its own.\n */\nexport function uuidv7(now: number = Date.now()): string {\n const bytes = randomBytes(16);\n bytes[0] = Math.floor(now / 2 ** 40) & 0xff;\n bytes[1] = Math.floor(now / 2 ** 32) & 0xff;\n bytes[2] = Math.floor(now / 2 ** 24) & 0xff;\n bytes[3] = Math.floor(now / 2 ** 16) & 0xff;\n bytes[4] = Math.floor(now / 2 ** 8) & 0xff;\n bytes[5] = now & 0xff;\n bytes[6] = (bytes[6]! & 0x0f) | 0x70;\n bytes[8] = (bytes[8]! & 0x3f) | 0x80;\n const hex = bytes.toString('hex');\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n","import { Kysely, type ColumnType, type Expression, type Generated } from 'kysely';\nimport { PostgresJSDialect } from 'kysely-postgres-js';\nimport postgres, { type Sql } from 'postgres';\nimport { ContactsLayoutError, missingMigrations } from './migrate.js';\nimport type { MigrationSource } from './migrations/index.js';\n\ntype Timestamp = ColumnType<Date, Date | string | undefined, Date | string>;\n\nexport interface ContactsTable {\n id: string;\n tenant_id: string;\n kind: 'person' | 'organization';\n name: string;\n company_or_role: Generated<string>;\n organization_id: string | null;\n email: string | null;\n phone: string | null;\n note: string | null;\n legal_form: string | null;\n address_line1: string | null;\n address_line2: string | null;\n postal_code: string | null;\n city: string | null;\n country: string | null;\n vat_id: string | null;\n customer_number: number | null;\n preferred_contact: string | null;\n /** Written as a cast JSON expression (see columns in contacts.ts); read back as the parsed object. */\n custom_fields: ColumnType<Record<string, unknown>, Expression<unknown> | undefined, Expression<unknown>>;\n identity_key: string;\n version: Generated<number>;\n archived_at: ColumnType<Date | null, Date | string | null | undefined, Date | string | null>;\n created_at: Timestamp;\n updated_at: Timestamp;\n}\n\nexport interface ContactFieldDefinitionsTable {\n tenant_id: string;\n id: string;\n key: string;\n label: string;\n type: string;\n /** Written as a cast JSON expression; read back as the parsed array (null for non-choice types). */\n options: ColumnType<string[] | null, Expression<unknown> | null | undefined, Expression<unknown> | null>;\n archived_at: ColumnType<Date | null, Date | string | null | undefined, Date | string | null>;\n created_at: Timestamp;\n}\n\nexport interface TenantCountersTable {\n tenant_id: string;\n next_customer_number: Generated<number>;\n}\n\nexport interface Database {\n contacts: ContactsTable;\n contact_field_definitions: ContactFieldDefinitionsTable;\n tenant_counters: TenantCountersTable;\n}\n\n/**\n * One app's handle on the contacts database. Create it once per process.\n *\n * Nothing connects until the first query, so an app starts and runs while the\n * contacts database is unreachable; only the calls that need it fail.\n */\nexport interface ContactsDb {\n readonly sql: Sql;\n readonly db: Kysely<Database>;\n /**\n * Resolves once the database has every migration this build needs. Throws\n * ContactsLayoutError when it is behind. A database that is ahead is fine.\n * The positive answer is remembered; a failure is asked again next time.\n */\n ready(): Promise<void>;\n close(): Promise<void>;\n}\n\nexport interface ContactsDbOptions {\n /** Shown in the database's connection list and logs, e.g. \"receipts\". Required: it is how a write is attributed. */\n applicationName: string;\n /** Connections this process may hold. Keep it small: many apps share one small instance. Default 3. */\n max?: number;\n /** Seconds before an idle connection is closed. Default 20. */\n idleTimeoutSeconds?: number;\n /** Seconds to wait for a connection before failing. Default 10. */\n connectTimeoutSeconds?: number;\n /** Tests only: the migrations `ready()` checks for. */\n migrations?: readonly MigrationSource[];\n}\n\nexport function createContactsDb(connectionString: string, options: ContactsDbOptions): ContactsDb {\n if (!connectionString) throw new TypeError('createContactsDb: a connection string is required');\n if (!options?.applicationName) throw new TypeError('createContactsDb: applicationName is required');\n const sql = postgres(connectionString, {\n max: options.max ?? 3,\n idle_timeout: options.idleTimeoutSeconds ?? 20,\n connect_timeout: options.connectTimeoutSeconds ?? 10,\n connection: { application_name: `contacts:${options.applicationName}`.slice(0, 63) },\n onnotice: () => {},\n });\n const db = new Kysely<Database>({ dialect: new PostgresJSDialect({ postgres: sql }) });\n\n let checked: Promise<void> | null = null;\n const ready = (): Promise<void> => {\n if (!checked) {\n const attempt = missingMigrations(sql, options.migrations).then((missing) => {\n if (missing.length > 0) throw new ContactsLayoutError(missing);\n });\n checked = attempt;\n // A failure (database down, layout behind) is not remembered.\n attempt.catch(() => {\n if (checked === attempt) checked = null;\n });\n }\n return checked;\n };\n\n return {\n sql,\n db,\n ready,\n close: async () => {\n await db.destroy();\n await sql.end({ timeout: 5 });\n },\n };\n}\n","import type { Contacts } from './contacts.js';\nimport { ContactError, normalizeContactInput, type ContactInput, type ContactKind } from './model.js';\n\n/**\n * Importing an existing customer list (for example the clients that already\n * carry a hand-assigned customer number).\n *\n * The file is personal data: it lives outside every repository, and nothing\n * here prints a name. Reports speak in row numbers and error codes only.\n */\n\nexport const IMPORT_COLUMNS = [\n 'customer_number',\n 'kind',\n 'name',\n 'company_or_role',\n 'legal_form',\n 'address_line1',\n 'address_line2',\n 'postal_code',\n 'city',\n 'country',\n 'vat_id',\n 'email',\n 'phone',\n 'note',\n] as const;\n\nexport type ImportRow = Partial<Record<(typeof IMPORT_COLUMNS)[number], string>>;\n\nexport class CsvError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'CsvError';\n }\n}\n\n/** A small RFC 4180 reader: comma separated, double quotes, doubled quote inside quotes, first line is the header. */\nexport function parseCsv(text: string): ImportRow[] {\n const records: string[][] = [];\n let field = '';\n let record: string[] = [];\n let quoted = false;\n const src = text.replace(/^/, '');\n for (let i = 0; i < src.length; i++) {\n const c = src[i]!;\n if (quoted) {\n if (c === '\"') {\n if (src[i + 1] === '\"') {\n field += '\"';\n i++;\n } else quoted = false;\n } else field += c;\n } else if (c === '\"') {\n if (field.length > 0) throw new CsvError(`line ${records.length + 1}: a quote inside an unquoted field`);\n quoted = true;\n } else if (c === ',') {\n record.push(field);\n field = '';\n } else if (c === '\\n' || c === '\\r') {\n if (c === '\\r' && src[i + 1] === '\\n') i++;\n record.push(field);\n field = '';\n records.push(record);\n record = [];\n } else field += c;\n }\n if (quoted) throw new CsvError('the file ends inside a quoted field');\n if (field.length > 0 || record.length > 0) {\n record.push(field);\n records.push(record);\n }\n const nonEmpty = records.filter((r) => r.some((f) => f.trim() !== ''));\n const header = nonEmpty.shift();\n if (!header) throw new CsvError('the file is empty');\n const names = header.map((h) => h.trim().toLowerCase());\n const allowed = new Set<string>(IMPORT_COLUMNS);\n for (const n of names) if (!allowed.has(n)) throw new CsvError(`unknown column \"${n}\"`);\n if (!names.includes('name')) throw new CsvError('the column \"name\" is required');\n if (new Set(names).size !== names.length) throw new CsvError('a column appears twice');\n return nonEmpty.map((r, index) => {\n if (r.length > names.length) throw new CsvError(`row ${index + 2}: more fields than columns`);\n const row: ImportRow = {};\n names.forEach((n, i) => {\n row[n as (typeof IMPORT_COLUMNS)[number]] = (r[i] ?? '').trim();\n });\n return row;\n });\n}\n\nexport type ImportOutcome = 'created' | 'numbered' | 'unchanged';\n\nexport interface ImportReport {\n rows: number;\n created: number;\n /** An existing contact that received its customer number. */\n numbered: number;\n unchanged: number;\n /** `row` is the line in the file (the header is line 1). */\n errors: { row: number; code: string; field?: string }[];\n nextCustomerNumber: number;\n}\n\nfunction toInput(row: ImportRow): { input: ContactInput; customerNumber: number | null } {\n const kindRaw = (row.kind ?? '').toLowerCase() || 'organization';\n if (kindRaw !== 'person' && kindRaw !== 'organization') throw new ContactError('invalid_field', { field: 'kind' });\n let customerNumber: number | null = null;\n if (row.customer_number) {\n if (!/^\\d{1,9}$/.test(row.customer_number)) throw new ContactError('invalid_field', { field: 'customer_number' });\n customerNumber = Number.parseInt(row.customer_number, 10);\n if (customerNumber < 1) throw new ContactError('invalid_field', { field: 'customer_number' });\n }\n return {\n customerNumber,\n input: {\n kind: kindRaw as ContactKind,\n name: row.name ?? '',\n companyOrRole: row.company_or_role,\n legalForm: row.legal_form,\n addressLine1: row.address_line1,\n addressLine2: row.address_line2,\n postalCode: row.postal_code,\n city: row.city,\n country: row.country,\n vatId: row.vat_id,\n email: row.email,\n phone: row.phone,\n note: row.note,\n },\n };\n}\n\n/**\n * Creates the contacts of `rows` and sets their customer numbers. Safe to run\n * again: a contact that exists (same kind, name and company) is not created a\n * second time and its fields are left alone; only a missing customer number is\n * set. With `dryRun` nothing is written and the report says what would happen.\n */\nexport async function importContacts(\n contacts: Contacts,\n rows: readonly ImportRow[],\n options: { dryRun?: boolean } = {},\n): Promise<ImportReport> {\n const report: ImportReport = {\n rows: rows.length,\n created: 0,\n numbered: 0,\n unchanged: 0,\n errors: [],\n nextCustomerNumber: 1,\n };\n const numbersInFile = new Map<number, number>();\n for (const [index, row] of rows.entries()) {\n const line = index + 2;\n try {\n const { input, customerNumber } = toInput(row);\n normalizeContactInput(input);\n if (customerNumber !== null) {\n const firstLine = numbersInFile.get(customerNumber);\n if (firstLine !== undefined) throw new ContactError('customer_number_conflict');\n numbersInFile.set(customerNumber, line);\n }\n const existing = await contacts.findSame(input);\n if (existing) {\n if (customerNumber === null || existing.customerNumber === customerNumber) {\n report.unchanged++;\n continue;\n }\n if (existing.customerNumber !== null) throw new ContactError('customer_number_conflict');\n if (!options.dryRun) await contacts.setCustomerNumber(existing.id, customerNumber);\n report.numbered++;\n continue;\n }\n if (!options.dryRun) {\n const created = await contacts.create(input);\n if (customerNumber !== null) {\n try {\n await contacts.setCustomerNumber(created.id, customerNumber);\n } catch (e) {\n // Do not leave a contact behind whose number could not be set.\n await contacts.erase(created.id);\n throw e;\n }\n }\n }\n report.created++;\n } catch (e) {\n if (e instanceof ContactError) report.errors.push({ row: line, code: e.code, field: e.field });\n else throw e;\n }\n }\n const stored = await contacts.peekNextCustomerNumber();\n const highestInFile = numbersInFile.size > 0 ? Math.max(...numbersInFile.keys()) : 0;\n report.nextCustomerNumber = options.dryRun ? Math.max(stored, highestInFile + 1) : stored;\n return report;\n}\n"],"mappings":"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","names":["collapse","sql"]}
|