Files
zeekayandHanzo Dev 92b2a43c17 fix(prisma): finish the move to Prisma 7, and keep DateTime an integer
The client and CLI were raised to Prisma 7 without the migration that major
requires, so `prisma generate` stopped at P1012 and nothing downstream of it
could build.

Prisma 7 takes the connection URL out of the schema and has no query engine:
the CLI reads prisma.config.ts, and the client is handed a driver adapter.
Both read the one DATABASE_URL. The adapter holds a single connection, which
is the single writer SQLite wants by construction — the `?connection_limit=1`
suffix the old engine needed is gone, and had to be: the adapter strips
`file:` and takes the rest as the file's name.

THE TRAP IS THE ADAPTER'S DEFAULT. The engine this replaces wrote every
DateTime as an epoch-millisecond INTEGER; that is what a deployed sign.db
holds and what sqlite-sql.ts divides by 1000 to read. The better-sqlite3
adapter defaults to ISO-8601 TEXT. Left alone it would write strings into
columns full of integers, SQLite would order them by storage class rather
than by time, and nothing would throw. `timestampFormat: 'unixepoch-ms'`
keeps one representation, and timestamp.test.ts reads the column's storage
class over a second connection. Checked both ways: on the default it fails
with `createdAt is stored as text: 2026-03-04T05:06:07.890+00:00`.

packages/prisma: 52 of 52.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-18 17:58:13 -07:00

47 lines
2.2 KiB
TypeScript

/// <reference types="@hanzo/esign-tsconfig/process-env.d.ts" />
/**
* Database resolution for the Base SQLite store.
*
* esign is a single-binary multi-tenant app. Identity is global: a `User` owns
* and belongs to MANY organisations (`User.ownedOrganisations[]` /
* `organisationMember[]`), and `Session` / `Account` / `Passkey` / `ApiToken`
* carry no `organisationId`. Auth is resolved BEFORE any org is known —
* `validateSessionToken` reads `Session` to discover the user — so a database
* cannot be selected per-org at request entry (the chicken-and-egg the prior
* file-per-org design could never satisfy). Only 8 of 47 tables even carry an
* `organisationId`; the rest scope through relations (`Envelope → Team →
* Organisation`) that a per-file split would make un-joinable.
*
* Therefore there is ONE Base SQLite database. Tenant isolation is enforced
* where it has always lived in this schema: row-level org/team scoping in the
* `packages/lib/server-only/*` handlers — every cross-org read funnels through
* the single `buildTeamWhereQuery({ teamId, userId })` predicate keyed on the
* authenticated user, never on a client-supplied value. That predicate, not a
* filesystem boundary, is the isolation boundary. See
* `packages/prisma/__tests__/tenant-isolation.test.ts` for the proof that a
* forged team/org id returns zero rows.
*
* This module owns the ONE place a SQLite connection URL is constructed, and
* fails closed if it cannot be resolved unambiguously.
*/
/**
* The Prisma datasource URL for the Base SQLite store.
*
* Single source of truth: `DATABASE_URL`. Fails closed when unset — there is no
* implicit fallback path that could silently point production at the wrong
* file. It is returned as given: the driver adapter opens exactly one
* connection, so there is one writer without asking for one. (The query engine
* this replaced pooled connections, and needed `?connection_limit=1` appended
* to get the same thing; the adapter would read that suffix as part of the
* file's NAME.)
*/
export const databaseUrl = (): string => {
const url = process.env.DATABASE_URL;
if (!url) {
throw new Error('DATABASE_URL is not set — cannot resolve the SQLite database.');
}
return url;
};