mirror of
https://github.com/hanzoai/sign.git
synced 2026-10-21 01:21:51 +00:00
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]>
139 lines
5.4 KiB
TypeScript
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';
|