@dataql/prisma-adapter
Version:
Prisma adapter for DataQL with zero API changes
745 lines (634 loc) • 19.4 kB
text/typescript
import {
Data,
DataOptions,
ID,
String,
Int,
Boolean,
Decimal,
Date as DataQLDate,
} from "@dataql/core";
// Prisma-like types and interfaces
export interface PrismaClientOptions {
// Required for DataQL infrastructure routing
appToken?: string;
// Database name for client isolation
dbName?: string;
// Environment routing
env?: "dev" | "prod";
// Development prefix
devPrefix?: string;
// Optional custom connection
customConnection?: any;
// Prisma-specific options (for compatibility)
log?: Array<"query" | "info" | "warn" | "error">;
errorFormat?: "pretty" | "colorless" | "minimal";
datasources?: {
db?: {
url?: string;
};
};
}
// Prisma field types
export type PrismaFieldType =
| "String"
| "Boolean"
| "Int"
| "BigInt"
| "Float"
| "Decimal"
| "DateTime"
| "Json"
| "Bytes"
| "Uuid";
// Prisma query options
export interface FindManyArgs<T = any> {
select?: any;
include?: any;
where?: WhereInput<T>;
orderBy?: OrderByInput<T> | OrderByInput<T>[];
cursor?: any;
take?: number;
skip?: number;
distinct?: string | string[];
}
export interface FindUniqueArgs<T = any> {
select?: any;
include?: any;
where: WhereUniqueInput<T>;
}
export interface FindFirstArgs<T = any> {
select?: any;
include?: any;
where?: WhereInput<T>;
orderBy?: OrderByInput<T> | OrderByInput<T>[];
cursor?: any;
take?: number;
skip?: number;
distinct?: string | string[];
}
export interface CreateArgs<T = any> {
data: CreateInput<T>;
select?: any;
include?: any;
}
export interface CreateManyArgs<T = any> {
data: CreateInput<T> | CreateInput<T>[];
skipDuplicates?: boolean;
}
export interface UpdateArgs<T = any> {
where: WhereUniqueInput<T>;
data: UpdateInput<T>;
select?: any;
include?: any;
}
export interface UpdateManyArgs<T = any> {
where?: WhereInput<T>;
data: UpdateInput<T>;
}
export interface UpsertArgs<T = any> {
where: WhereUniqueInput<T>;
create: CreateInput<T>;
update: UpdateInput<T>;
select?: any;
include?: any;
}
export interface DeleteArgs<T = any> {
where: WhereUniqueInput<T>;
select?: any;
include?: any;
}
export interface DeleteManyArgs<T = any> {
where?: WhereInput<T>;
}
export interface CountArgs<T = any> {
select?: any;
where?: WhereInput<T>;
}
export interface AggregateArgs<T = any> {
where?: WhereInput<T>;
orderBy?: OrderByInput<T> | OrderByInput<T>[];
cursor?: any;
take?: number;
skip?: number;
_count?: boolean | CountSelect<T>;
_avg?: AverageSelect<T>;
_sum?: SumSelect<T>;
_min?: MinSelect<T>;
_max?: MaxSelect<T>;
}
// Where clause types
export type WhereInput<T> = {
[K in keyof T]?: T[K] extends string
? StringFilter | T[K]
: T[K] extends number
? NumberFilter | T[K]
: T[K] extends boolean
? BooleanFilter | T[K]
: T[K] extends Date
? DateTimeFilter | T[K]
: any;
} & {
AND?: WhereInput<T> | WhereInput<T>[];
OR?: WhereInput<T>[];
NOT?: WhereInput<T> | WhereInput<T>[];
};
export type WhereUniqueInput<T> = {
[K in keyof T]?: T[K];
};
export interface StringFilter {
equals?: string;
in?: string[];
notIn?: string[];
lt?: string;
lte?: string;
gt?: string;
gte?: string;
contains?: string;
startsWith?: string;
endsWith?: string;
mode?: "default" | "insensitive";
not?: string | StringFilter;
}
export interface NumberFilter {
equals?: number;
in?: number[];
notIn?: number[];
lt?: number;
lte?: number;
gt?: number;
gte?: number;
not?: number | NumberFilter;
}
export interface BooleanFilter {
equals?: boolean;
not?: boolean | BooleanFilter;
}
export interface DateTimeFilter {
equals?: Date | string;
in?: (Date | string)[];
notIn?: (Date | string)[];
lt?: Date | string;
lte?: Date | string;
gt?: Date | string;
gte?: Date | string;
not?: Date | string | DateTimeFilter;
}
// Order by types
export type OrderByInput<T> = {
[K in keyof T]?: "asc" | "desc";
};
// Input types
export type CreateInput<T> = {
[K in keyof T]?: T[K] extends Array<infer U>
? CreateNestedManyInput<U>
: T[K];
};
export type UpdateInput<T> = {
[K in keyof T]?: T[K] extends Array<infer U>
? UpdateNestedManyInput<U>
: T[K];
};
export interface CreateNestedManyInput<T> {
create?: CreateInput<T> | CreateInput<T>[];
connect?: WhereUniqueInput<T> | WhereUniqueInput<T>[];
}
export interface UpdateNestedManyInput<T> {
create?: CreateInput<T> | CreateInput<T>[];
connect?: WhereUniqueInput<T> | WhereUniqueInput<T>[];
set?: WhereUniqueInput<T>[];
disconnect?: WhereUniqueInput<T> | WhereUniqueInput<T>[];
delete?: WhereUniqueInput<T> | WhereUniqueInput<T>[];
update?: UpdateWithWhereUniqueInput<T> | UpdateWithWhereUniqueInput<T>[];
updateMany?: UpdateManyWithWhereInput<T> | UpdateManyWithWhereInput<T>[];
deleteMany?: WhereInput<T> | WhereInput<T>[];
upsert?: UpsertWithWhereUniqueInput<T> | UpsertWithWhereUniqueInput<T>[];
}
export interface UpdateWithWhereUniqueInput<T> {
where: WhereUniqueInput<T>;
data: UpdateInput<T>;
}
export interface UpdateManyWithWhereInput<T> {
where: WhereInput<T>;
data: UpdateInput<T>;
}
export interface UpsertWithWhereUniqueInput<T> {
where: WhereUniqueInput<T>;
create: CreateInput<T>;
update: UpdateInput<T>;
}
// Aggregate types
export type CountSelect<T> =
| boolean
| {
[K in keyof T]?: boolean;
};
export type AverageSelect<T> = {
[K in keyof T]?: T[K] extends number ? boolean : never;
};
export type SumSelect<T> = {
[K in keyof T]?: T[K] extends number ? boolean : never;
};
export type MinSelect<T> = {
[K in keyof T]?: boolean;
};
export type MaxSelect<T> = {
[K in keyof T]?: boolean;
};
// Result types
export interface BatchPayload {
count: number;
}
export interface AggregateResult<T> {
_count?: { [K in keyof T]?: number } | number;
_avg?: { [K in keyof T]?: number };
_sum?: { [K in keyof T]?: number };
_min?: { [K in keyof T]?: any };
_max?: { [K in keyof T]?: any };
}
// Prisma model delegate
export class PrismaModelDelegate<T> {
private _collection: any;
private _modelName: string;
private _data: Data;
constructor(data: Data, modelName: string, schema: any) {
this._data = data;
this._modelName = modelName;
this._collection = data.collection(modelName, schema);
}
async findMany(args?: FindManyArgs<T>): Promise<T[]> {
const filter = this._convertWhereToDataQL(args?.where);
let results = await this._collection.find(filter);
// Apply ordering
if (args?.orderBy) {
results = this._applyOrderBy(results, args.orderBy);
}
// Apply pagination
if (args?.skip) {
results = results.slice(args.skip);
}
if (args?.take) {
results = results.slice(0, args.take);
}
// Apply selection
if (args?.select) {
results = results.map((item: any) =>
this._applySelect(item, args.select)
);
}
return results;
}
async findUnique(args: FindUniqueArgs<T>): Promise<T | null> {
const filter = this._convertWhereToDataQL(args.where);
const results = await this._collection.find(filter);
if (results.length === 0) return null;
let result = results[0];
if (args.select) {
result = this._applySelect(result, args.select);
}
return result;
}
async findFirst(args?: FindFirstArgs<T>): Promise<T | null> {
const findManyArgs: FindManyArgs<T> = {
...args,
take: 1,
};
const results = await this.findMany(findManyArgs);
return results.length > 0 ? results[0] : null;
}
async create(args: CreateArgs<T>): Promise<T> {
const result = await this._collection.create(args.data);
if (args.select) {
return this._applySelect(result, args.select);
}
return result;
}
async createMany(args: CreateManyArgs<T>): Promise<BatchPayload> {
const data = Array.isArray(args.data) ? args.data : [args.data];
const results = await this._collection.create(data);
return {
count: Array.isArray(results) ? results.length : 1,
};
}
async update(args: UpdateArgs<T>): Promise<T> {
const filter = this._convertWhereToDataQL(args.where);
const result = await this._collection.update(filter, args.data);
if (args.select) {
return this._applySelect(result, args.select);
}
return result;
}
async updateMany(args: UpdateManyArgs<T>): Promise<BatchPayload> {
const filter = this._convertWhereToDataQL(args.where);
const results = await this._collection.update(filter, args.data);
return {
count: Array.isArray(results) ? results.length : 1,
};
}
async upsert(args: UpsertArgs<T>): Promise<T> {
const filter = this._convertWhereToDataQL(args.where);
const existing = await this._collection.find(filter);
let result;
if (existing.length > 0) {
result = await this._collection.update(filter, args.update);
} else {
result = await this._collection.create(args.create);
}
if (args.select) {
return this._applySelect(result, args.select);
}
return result;
}
async delete(args: DeleteArgs<T>): Promise<T> {
const filter = this._convertWhereToDataQL(args.where);
const existing = await this._collection.find(filter);
if (existing.length === 0) {
throw new Error("Record to delete does not exist.");
}
await this._collection.delete(filter);
let result = existing[0];
if (args.select) {
result = this._applySelect(result, args.select);
}
return result;
}
async deleteMany(args?: DeleteManyArgs<T>): Promise<BatchPayload> {
const filter = this._convertWhereToDataQL(args?.where);
const existing = await this._collection.find(filter);
const count = existing.length;
if (count > 0) {
await this._collection.delete(filter);
}
return { count };
}
async count(args?: CountArgs<T>): Promise<number> {
const filter = this._convertWhereToDataQL(args?.where);
const results = await this._collection.find(filter);
return results.length;
}
async aggregate(args?: AggregateArgs<T>): Promise<AggregateResult<T>> {
const filter = this._convertWhereToDataQL(args?.where);
let results = await this._collection.find(filter);
// Apply ordering
if (args?.orderBy) {
results = this._applyOrderBy(results, args.orderBy);
}
// Apply pagination
if (args?.skip) {
results = results.slice(args.skip);
}
if (args?.take) {
results = results.slice(0, args.take);
}
const aggregateResult: AggregateResult<T> = {};
// Count
if (args?._count) {
if (typeof args._count === "boolean") {
aggregateResult._count = results.length;
} else {
const countResult: any = {};
for (const field in args._count) {
if (args._count[field]) {
countResult[field] = results.filter(
(item: any) => item[field] !== null && item[field] !== undefined
).length;
}
}
aggregateResult._count = countResult;
}
}
// Other aggregations (avg, sum, min, max) would need more sophisticated implementation
// For now, we'll provide basic implementations
return aggregateResult;
}
private _convertWhereToDataQL(where?: any): any {
if (!where) return {};
const result: any = {};
for (const [key, value] of Object.entries(where)) {
if (key === "AND") {
// Handle AND conditions
const conditions = Array.isArray(value) ? value : [value];
for (const condition of conditions) {
Object.assign(result, this._convertWhereToDataQL(condition));
}
} else if (key === "OR") {
// Handle OR conditions (simplified - DataQL might need native OR support)
result.$or = Array.isArray(value)
? value.map((v: any) => this._convertWhereToDataQL(v))
: [this._convertWhereToDataQL(value)];
} else if (key === "NOT") {
// Handle NOT conditions (simplified - DataQL might need native NOT support)
result.$not = this._convertWhereToDataQL(value);
} else if (typeof value === "object" && value !== null) {
// Handle field filters
const fieldFilter: any = {};
for (const [filterKey, filterValue] of Object.entries(value)) {
switch (filterKey) {
case "equals":
result[key] = filterValue;
break;
case "in":
result[key] = { $in: filterValue };
break;
case "notIn":
result[key] = { $nin: filterValue };
break;
case "lt":
result[key] = { $lt: filterValue };
break;
case "lte":
result[key] = { $lte: filterValue };
break;
case "gt":
result[key] = { $gt: filterValue };
break;
case "gte":
result[key] = { $gte: filterValue };
break;
case "contains":
result[key] = { $regex: filterValue, $options: "i" };
break;
case "startsWith":
result[key] = { $regex: `^${filterValue}`, $options: "i" };
break;
case "endsWith":
result[key] = { $regex: `${filterValue}$`, $options: "i" };
break;
case "not":
result[key] = { $ne: filterValue };
break;
default:
// Default case
result[key] = { [`$${filterKey}`]: filterValue };
}
}
} else {
// Direct value
result[key] = value;
}
}
return result;
}
private _applyOrderBy(
results: any[],
orderBy: OrderByInput<T> | OrderByInput<T>[]
): any[] {
const orderBys = Array.isArray(orderBy) ? orderBy : [orderBy];
return results.sort((a, b) => {
for (const order of orderBys) {
for (const [field, direction] of Object.entries(order)) {
const aVal = a[field];
const bVal = b[field];
if (aVal < bVal) return direction === "asc" ? -1 : 1;
if (aVal > bVal) return direction === "asc" ? 1 : -1;
}
}
return 0;
});
}
private _applySelect(item: any, select: any): any {
if (!select) return item;
const result: any = {};
for (const [key, value] of Object.entries(select)) {
if (value === true) {
result[key] = item[key];
} else if (typeof value === "object") {
// Nested selection (simplified)
result[key] = item[key];
}
}
return result;
}
}
// Main Prisma client class
export class PrismaClient {
private _data: Data;
private _models: Map<string, PrismaModelDelegate<any>> = new Map();
private _schemas: Map<string, any> = new Map();
constructor(options?: PrismaClientOptions) {
// Ensure proper DataQL infrastructure routing (Client → Worker → Lambda → Database)
const dataqlOptions = {
appToken: options?.appToken || "default", // Required for DataQL routing
env: options?.env || "prod", // Routes to production infrastructure
devPrefix: options?.devPrefix || "dev_", // Optional prefix for dev environments
dbName: options?.dbName, // Database isolation per client
customConnection: options?.customConnection, // Optional custom connection
};
this._data = new Data(dataqlOptions);
}
// Register a model with schema
_registerModel<T>(name: string, schema: any): PrismaModelDelegate<T> {
const delegate = new PrismaModelDelegate<T>(this._data, name, schema);
this._models.set(name, delegate);
this._schemas.set(name, schema);
return delegate;
}
// Get a model delegate
_getModel<T>(name: string): PrismaModelDelegate<T> {
const delegate = this._models.get(name);
if (!delegate) {
throw new Error(
`Model ${name} has not been registered. Use _registerModel() first.`
);
}
return delegate;
}
// Transaction support
async $transaction<T>(fn: (prisma: PrismaClient) => Promise<T>): Promise<T>;
async $transaction<T>(queries: any[]): Promise<any[]>;
async $transaction<T>(
fnOrQueries: ((prisma: PrismaClient) => Promise<T>) | any[]
): Promise<T | any[]> {
if (typeof fnOrQueries === "function") {
// Interactive transaction
return await this._data.transaction(async () => {
return await fnOrQueries(this);
});
} else {
// Batch transaction
return await this._data.transaction(async () => {
const results: any[] = [];
for (const query of fnOrQueries) {
const result = await query;
results.push(result);
}
return results;
});
}
}
// Raw query methods (mocked for now)
async $queryRaw<T = any>(
query: TemplateStringsArray,
...values: any[]
): Promise<T> {
// This would need to be implemented based on DataQL's raw query capabilities
throw new Error("Raw queries not yet supported in DataQL Prisma adapter");
}
async $executeRaw(
query: TemplateStringsArray,
...values: any[]
): Promise<number> {
// This would need to be implemented based on DataQL's raw query capabilities
throw new Error("Raw queries not yet supported in DataQL Prisma adapter");
}
// Connection management
async $connect(): Promise<void> {
// DataQL connections are handled automatically
return Promise.resolve();
}
async $disconnect(): Promise<void> {
// DataQL connections are handled automatically
return Promise.resolve();
}
}
// Helper function to create a Prisma client with schema registration
export function createPrismaClient<T extends Record<string, any>>(
options?: PrismaClientOptions,
schemas?: { [K in keyof T]: any }
): PrismaClient & { [K in keyof T]: PrismaModelDelegate<T[K]> } {
const client = new PrismaClient(options);
if (schemas) {
for (const [modelName, schema] of Object.entries(schemas)) {
const delegate = client._registerModel(modelName, schema);
(client as any)[modelName] = delegate;
}
}
return client as PrismaClient & { [K in keyof T]: PrismaModelDelegate<T[K]> };
}
// Error classes
export class PrismaClientKnownRequestError extends Error {
code: string;
meta?: Record<string, any>;
constructor(message: string, code: string, meta?: Record<string, any>) {
super(message);
this.name = "PrismaClientKnownRequestError";
this.code = code;
this.meta = meta;
}
}
export class PrismaClientUnknownRequestError extends Error {
constructor(message: string) {
super(message);
this.name = "PrismaClientUnknownRequestError";
}
}
export class PrismaClientRustPanicError extends Error {
constructor(message: string) {
super(message);
this.name = "PrismaClientRustPanicError";
}
}
export class PrismaClientInitializationError extends Error {
constructor(message: string) {
super(message);
this.name = "PrismaClientInitializationError";
}
}
export class PrismaClientValidationError extends Error {
constructor(message: string) {
super(message);
this.name = "PrismaClientValidationError";
}
}
// Export the main client
export default PrismaClient;