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

139 lines
5.4 KiB
TypeScript

/// <reference types="@hanzo/esign-prisma/types/types.d.ts" />
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3';
import { Prisma, PrismaClient } from '@prisma/client';
import type { Role, WebhookTriggerEvents } from '@prisma/client';
import { Kysely, SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from 'kysely';
import kyselyExtension from 'prisma-extension-kysely';
import type { DB } from './generated/types';
import { decodeList, encodeListFields } from './json-array';
import { databaseUrl } from './tenant';
import { remember } from './utils/remember';
/**
* Base SQLite store. One process-wide Prisma client bound to `DATABASE_URL`
* (see `./tenant.ts`). Tenant isolation is row-level org/team scoping in the
* `server-only/*` handlers, not a per-connection database — identity is global
* here (a user spans many orgs), so the database cannot be chosen per request.
*/
// --- list-field codec extension -------------------------------------------
// The single bridge for the four columns that were Postgres arrays and are now
// JSON `TEXT` in SQLite. `result` decodes (and re-types) each column back to a
// typed array on read; `query` encodes arrays to JSON on every write. Written
// statically per model so Prisma infers the array result types end-to-end —
// `prisma.user.findX().roles` is `Role[]`, not `string`.
const listFieldExtension = Prisma.defineExtension({
name: 'sqlite-list-fields',
result: {
user: {
roles: {
needs: { roles: true },
compute: ({ roles }): Role[] => decodeList<Role>(roles),
},
},
webhook: {
eventTriggers: {
needs: { eventTriggers: true },
compute: ({ eventTriggers }): WebhookTriggerEvents[] =>
decodeList<WebhookTriggerEvents>(eventTriggers),
},
},
passkey: {
transports: {
needs: { transports: true },
compute: ({ transports }): string[] => decodeList<string>(transports),
},
},
organisationAuthenticationPortal: {
allowedDomains: {
needs: { allowedDomains: true },
compute: ({ allowedDomains }): string[] => decodeList<string>(allowedDomains),
},
},
},
query: {
user: {
$allOperations: async ({ args, query }) => (encodeListFields('User', args), query(args)),
},
webhook: {
$allOperations: async ({ args, query }) => (encodeListFields('Webhook', args), query(args)),
},
passkey: {
$allOperations: async ({ args, query }) => (encodeListFields('Passkey', args), query(args)),
},
organisationAuthenticationPortal: {
$allOperations: async ({ args, query }) => (
encodeListFields('OrganisationAuthenticationPortal', args),
query(args)
),
},
},
});
// The driver. Prisma 7 has no query engine of its own: the client is handed an
// adapter and the adapter holds the connection — ONE connection here, which is
// the single writer SQLite wants, by construction rather than by a pool limit.
//
// `unixepoch-ms` IS NOT A PREFERENCE. The engine this replaces wrote every
// `DateTime` as an epoch-millisecond INTEGER, so that is what the rows in a
// deployed sign.db hold and what ./sqlite-sql.ts divides by 1000 to read. The
// adapter's default is ISO-8601 TEXT: left alone it would start writing strings
// into columns full of integers, every comparison across the two would order by
// type instead of by time, and nothing would fail — it would just answer wrong.
const buildClient = () =>
new PrismaClient({
adapter: new PrismaBetterSqlite3({ url: databaseUrl() }, { timestampFormat: 'unixepoch-ms' }),
}).$extends(listFieldExtension);
/** The extended client type, carrying the list-field array result types. */
export type ExtendedPrismaClient = ReturnType<typeof buildClient>;
/** The single process-wide client, built + memoised on first use. */
const client = (): ExtendedPrismaClient => remember('prisma', buildClient);
// Accessor proxy so importing this module does NOT open a DB connection — the
// client is constructed lazily on the first property access. This keeps `import
// { prisma }` side-effect-free for tooling/tests and lets DATABASE_URL be set
// before the first query. The 300+ `prisma.foo.bar()` call sites are unchanged.
export const prisma: ExtendedPrismaClient = new Proxy({} as ExtendedPrismaClient, {
get(_t, prop, receiver) {
const c = client();
const value = Reflect.get(c as object, prop, receiver);
return typeof value === 'function' ? value.bind(c) : value;
},
has(_t, prop) {
return Reflect.has(client() as object, prop);
},
}) as ExtendedPrismaClient;
// Kysely over the same client, with the SQLite dialect. Also lazy.
const buildKysely = () =>
client().$extends(
kyselyExtension({
kysely: (driver) =>
new Kysely<DB>({
dialect: {
createAdapter: () => new SqliteAdapter(),
createDriver: () => driver,
createIntrospector: (db) => new SqliteIntrospector(db),
createQueryCompiler: () => new SqliteQueryCompiler(),
},
}),
}),
);
type KyselyClient = ReturnType<typeof buildKysely>;
export const kyselyPrisma: KyselyClient = new Proxy({} as KyselyClient, {
get(_t, prop, receiver) {
const c = remember('kyselyPrisma', buildKysely);
const value = Reflect.get(c as object, prop, receiver);
return typeof value === 'function' ? value.bind(c) : value;
},
}) as KyselyClient;
export { sql } from 'kysely';
export { monthTrunc, epochMs } from './sqlite-sql';