248 lines
7.0 KiB
TypeScript
248 lines
7.0 KiB
TypeScript
import { type Cache, hashQuery, NoopCache } from '~/cache/core/cache.ts';
|
|
import type { WithCacheConfig } from '~/cache/core/types.ts';
|
|
import { entityKind, is } from '~/entity.ts';
|
|
import { DrizzleQueryError, TransactionRollbackError } from '~/errors.ts';
|
|
import type { TablesRelationalConfig } from '~/relations.ts';
|
|
import type { PreparedQuery } from '~/session.ts';
|
|
import type { Query, SQL } from '~/sql/index.ts';
|
|
import { tracer } from '~/tracing.ts';
|
|
import type { NeonAuthToken } from '~/utils.ts';
|
|
import { GelDatabase } from './db.ts';
|
|
import type { GelDialect } from './dialect.ts';
|
|
import type { SelectedFieldsOrdered } from './query-builders/select.types.ts';
|
|
|
|
export interface PreparedQueryConfig {
|
|
execute: unknown;
|
|
all: unknown;
|
|
values: unknown;
|
|
}
|
|
|
|
export abstract class GelPreparedQuery<T extends PreparedQueryConfig> implements PreparedQuery {
|
|
constructor(
|
|
protected query: Query,
|
|
private cache?: Cache,
|
|
// per query related metadata
|
|
private queryMetadata?: {
|
|
type: 'select' | 'update' | 'delete' | 'insert';
|
|
tables: string[];
|
|
} | undefined,
|
|
// config that was passed through $withCache
|
|
private cacheConfig?: WithCacheConfig,
|
|
) {
|
|
// it means that no $withCache options were passed and it should be just enabled
|
|
if (cache && cache.strategy() === 'all' && cacheConfig === undefined) {
|
|
this.cacheConfig = { enable: true, autoInvalidate: true };
|
|
}
|
|
if (!this.cacheConfig?.enable) {
|
|
this.cacheConfig = undefined;
|
|
}
|
|
}
|
|
|
|
/** @internal */
|
|
protected async queryWithCache<T>(
|
|
queryString: string,
|
|
params: any[],
|
|
query: () => Promise<T>,
|
|
): Promise<T> {
|
|
if (this.cache === undefined || is(this.cache, NoopCache) || this.queryMetadata === undefined) {
|
|
try {
|
|
return await query();
|
|
} catch (e) {
|
|
throw new DrizzleQueryError(queryString, params, e as Error);
|
|
}
|
|
}
|
|
|
|
// don't do any mutations, if globally is false
|
|
if (this.cacheConfig && !this.cacheConfig.enable) {
|
|
try {
|
|
return await query();
|
|
} catch (e) {
|
|
throw new DrizzleQueryError(queryString, params, e as Error);
|
|
}
|
|
}
|
|
|
|
// For mutate queries, we should query the database, wait for a response, and then perform invalidation
|
|
if (
|
|
(
|
|
this.queryMetadata.type === 'insert' || this.queryMetadata.type === 'update'
|
|
|| this.queryMetadata.type === 'delete'
|
|
) && this.queryMetadata.tables.length > 0
|
|
) {
|
|
try {
|
|
const [res] = await Promise.all([
|
|
query(),
|
|
this.cache.onMutate({ tables: this.queryMetadata.tables }),
|
|
]);
|
|
return res;
|
|
} catch (e) {
|
|
throw new DrizzleQueryError(queryString, params, e as Error);
|
|
}
|
|
}
|
|
|
|
// don't do any reads if globally disabled
|
|
if (!this.cacheConfig) {
|
|
try {
|
|
return await query();
|
|
} catch (e) {
|
|
throw new DrizzleQueryError(queryString, params, e as Error);
|
|
}
|
|
}
|
|
|
|
if (this.queryMetadata.type === 'select') {
|
|
const fromCache = await this.cache.get(
|
|
this.cacheConfig.tag ?? await hashQuery(queryString, params),
|
|
this.queryMetadata.tables,
|
|
this.cacheConfig.tag !== undefined,
|
|
this.cacheConfig.autoInvalidate,
|
|
);
|
|
if (fromCache === undefined) {
|
|
let result;
|
|
try {
|
|
result = await query();
|
|
} catch (e) {
|
|
throw new DrizzleQueryError(queryString, params, e as Error);
|
|
}
|
|
|
|
// put actual key
|
|
await this.cache.put(
|
|
this.cacheConfig.tag ?? await hashQuery(queryString, params),
|
|
result,
|
|
// make sure we send tables that were used in a query only if user wants to invalidate it on each write
|
|
this.cacheConfig.autoInvalidate ? this.queryMetadata.tables : [],
|
|
this.cacheConfig.tag !== undefined,
|
|
this.cacheConfig.config,
|
|
);
|
|
// put flag if we should invalidate or not
|
|
return result;
|
|
}
|
|
|
|
return fromCache as unknown as T;
|
|
}
|
|
try {
|
|
return await query();
|
|
} catch (e) {
|
|
throw new DrizzleQueryError(queryString, params, e as Error);
|
|
}
|
|
}
|
|
|
|
protected authToken?: NeonAuthToken;
|
|
|
|
getQuery(): Query {
|
|
return this.query;
|
|
}
|
|
|
|
mapResult(response: unknown, _isFromBatch?: boolean): unknown {
|
|
return response;
|
|
}
|
|
|
|
static readonly [entityKind]: string = 'GelPreparedQuery';
|
|
|
|
/** @internal */
|
|
joinsNotNullableMap?: Record<string, boolean>;
|
|
|
|
abstract execute(placeholderValues?: Record<string, unknown>): Promise<T['execute']>;
|
|
|
|
/** @internal */
|
|
abstract all(placeholderValues?: Record<string, unknown>): Promise<T['all']>;
|
|
|
|
/** @internal */
|
|
abstract isResponseInArrayMode(): boolean;
|
|
}
|
|
|
|
export abstract class GelSession<
|
|
TQueryResult extends GelQueryResultHKT = any, // TO
|
|
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
TSchema extends TablesRelationalConfig = Record<string, never>,
|
|
> {
|
|
static readonly [entityKind]: string = 'GelSession';
|
|
|
|
constructor(protected dialect: GelDialect) {}
|
|
|
|
abstract prepareQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
|
|
query: Query,
|
|
fields: SelectedFieldsOrdered | undefined,
|
|
name: string | undefined,
|
|
isResponseInArrayMode: boolean,
|
|
customResultMapper?: (rows: unknown[][], mapColumnValue?: (value: unknown) => unknown) => T['execute'],
|
|
queryMetadata?: {
|
|
type: 'select' | 'update' | 'delete' | 'insert';
|
|
tables: string[];
|
|
},
|
|
cacheConfig?: WithCacheConfig,
|
|
): GelPreparedQuery<T>;
|
|
|
|
execute<T>(query: SQL): Promise<T> {
|
|
return tracer.startActiveSpan('drizzle.operation', () => {
|
|
const prepared = tracer.startActiveSpan('drizzle.prepareQuery', () => {
|
|
return this.prepareQuery<PreparedQueryConfig & { execute: T }>(
|
|
this.dialect.sqlToQuery(query),
|
|
undefined,
|
|
undefined,
|
|
false,
|
|
);
|
|
});
|
|
|
|
return prepared.execute(undefined);
|
|
});
|
|
}
|
|
|
|
all<T = unknown>(query: SQL): Promise<T[]> {
|
|
return this.prepareQuery<PreparedQueryConfig & { all: T[] }>(
|
|
this.dialect.sqlToQuery(query),
|
|
undefined,
|
|
undefined,
|
|
false,
|
|
).all();
|
|
}
|
|
|
|
async count(sql: SQL): Promise<number> {
|
|
const res = await this.execute<[{ count: string }]>(sql);
|
|
|
|
return Number(
|
|
res[0]['count'],
|
|
);
|
|
}
|
|
|
|
abstract transaction<T>(
|
|
transaction: (tx: GelTransaction<TQueryResult, TFullSchema, TSchema>) => Promise<T>,
|
|
): Promise<T>;
|
|
}
|
|
|
|
export abstract class GelTransaction<
|
|
TQueryResult extends GelQueryResultHKT,
|
|
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
TSchema extends TablesRelationalConfig = Record<string, never>,
|
|
> extends GelDatabase<TQueryResult, TFullSchema, TSchema> {
|
|
static override readonly [entityKind]: string = 'GelTransaction';
|
|
|
|
constructor(
|
|
dialect: GelDialect,
|
|
session: GelSession<any, any, any>,
|
|
protected schema: {
|
|
fullSchema: Record<string, unknown>;
|
|
schema: TSchema;
|
|
tableNamesMap: Record<string, string>;
|
|
} | undefined,
|
|
) {
|
|
super(dialect, session, schema);
|
|
}
|
|
|
|
rollback(): never {
|
|
throw new TransactionRollbackError();
|
|
}
|
|
|
|
abstract override transaction<T>(
|
|
transaction: (tx: GelTransaction<TQueryResult, TFullSchema, TSchema>) => Promise<T>,
|
|
): Promise<T>;
|
|
}
|
|
|
|
export interface GelQueryResultHKT {
|
|
readonly $brand: 'GelQueryResultHKT';
|
|
readonly row: unknown;
|
|
readonly type: unknown;
|
|
}
|
|
|
|
export type GelQueryResultKind<TKind extends GelQueryResultHKT, TRow> = (TKind & {
|
|
readonly row: TRow;
|
|
})['type'];
|