update
This commit is contained in:
14
packages/runtime/src/utils/utils.compare.ts
Normal file
14
packages/runtime/src/utils/utils.compare.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import deepEqual from 'deep-equal';
|
||||
|
||||
const compareObjectKeys = <T extends Record<string, unknown>>(a: T, b: T, keys: (keyof T)[]) => {
|
||||
for (const key of keys) {
|
||||
const avalue = a[key];
|
||||
const bvalue = b[key];
|
||||
if (!deepEqual(avalue, bvalue)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export { compareObjectKeys };
|
||||
3
packages/runtime/src/utils/utils.consts.ts
Normal file
3
packages/runtime/src/utils/utils.consts.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
const EMBEDDING_MODEL = 'Xenova/all-MiniLM-L6-v2';
|
||||
|
||||
export { EMBEDDING_MODEL };
|
||||
66
packages/runtime/src/utils/utils.event-emitter.ts
Normal file
66
packages/runtime/src/utils/utils.event-emitter.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { ExplicitAny } from '#root/global.js';
|
||||
|
||||
type EventListener<T extends unknown[]> = (...args: T) => void | Promise<void>;
|
||||
|
||||
type OnOptions = {
|
||||
abortSignal?: AbortSignal;
|
||||
};
|
||||
|
||||
class EventEmitter<T extends Record<string, (...args: ExplicitAny[]) => void | Promise<void>>> {
|
||||
#listeners = new Map<keyof T, Set<EventListener<ExplicitAny>>>();
|
||||
|
||||
on = <K extends keyof T>(event: K, callback: EventListener<Parameters<T[K]>>, options: OnOptions = {}) => {
|
||||
const { abortSignal } = options;
|
||||
if (!this.#listeners.has(event)) {
|
||||
this.#listeners.set(event, new Set());
|
||||
}
|
||||
const callbackClone = (...args: Parameters<T[K]>) => callback(...args);
|
||||
const abortController = new AbortController();
|
||||
const listeners = this.#listeners.get(event);
|
||||
if (!listeners) {
|
||||
throw new Error('Event registration failed');
|
||||
}
|
||||
abortSignal?.addEventListener('abort', abortController.abort);
|
||||
listeners.add(callbackClone);
|
||||
abortController.signal.addEventListener('abort', () => {
|
||||
this.#listeners.set(event, listeners?.difference(new Set([callbackClone])));
|
||||
});
|
||||
return abortController.abort;
|
||||
};
|
||||
|
||||
once = <K extends keyof T>(event: K, callback: EventListener<Parameters<T[K]>>, options: OnOptions = {}) => {
|
||||
const abortController = new AbortController();
|
||||
options.abortSignal?.addEventListener('abort', abortController.abort);
|
||||
return this.on(
|
||||
event,
|
||||
async (...args) => {
|
||||
abortController.abort();
|
||||
await callback(...args);
|
||||
},
|
||||
{
|
||||
...options,
|
||||
abortSignal: abortController.signal,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
emit = <K extends keyof T>(event: K, ...args: Parameters<T[K]>) => {
|
||||
const listeners = this.#listeners.get(event);
|
||||
if (!listeners) {
|
||||
return;
|
||||
}
|
||||
for (const listener of listeners) {
|
||||
listener(...args);
|
||||
}
|
||||
};
|
||||
|
||||
emitAsync = async <K extends keyof T>(event: K, ...args: Parameters<T[K]>) => {
|
||||
const listeners = this.#listeners.get(event);
|
||||
if (!listeners) {
|
||||
return;
|
||||
}
|
||||
await Promise.all(listeners.values().map((listener) => listener(...args)));
|
||||
};
|
||||
}
|
||||
|
||||
export { EventEmitter };
|
||||
161
packages/runtime/src/utils/utils.query.ts
Normal file
161
packages/runtime/src/utils/utils.query.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import type {
|
||||
QueryCondition,
|
||||
QueryConditionNumber,
|
||||
QueryConditionText,
|
||||
QueryFilter,
|
||||
} from '@morten-olsen/stash-query-dsl';
|
||||
import { type Knex } from 'knex';
|
||||
/**
|
||||
* Escapes a JSON key for use in PostgreSQL JSON operators.
|
||||
* Escapes single quotes by doubling them, which is the PostgreSQL standard.
|
||||
*/
|
||||
const escapeJsonKey = (key: string): string => {
|
||||
return key.replace(/'/g, "''");
|
||||
};
|
||||
|
||||
const getFieldSelector = (query: Knex.QueryBuilder, field: string[], tableName?: string) => {
|
||||
const baseColumn = field[0];
|
||||
if (field.length === 1) {
|
||||
return tableName ? `${tableName}.${baseColumn}` : baseColumn;
|
||||
}
|
||||
|
||||
const baseFieldRef = tableName ? query.client.ref(baseColumn).withSchema(tableName) : query.client.ref(baseColumn);
|
||||
const jsonPath = field.slice(1);
|
||||
let sqlExpression = baseFieldRef.toString();
|
||||
|
||||
for (let i = 0; i < jsonPath.length - 1; i++) {
|
||||
const escapedKey = escapeJsonKey(jsonPath[i]);
|
||||
sqlExpression += ` -> '${escapedKey}'`;
|
||||
}
|
||||
|
||||
const finalElement = jsonPath[jsonPath.length - 1];
|
||||
const escapedFinalKey = escapeJsonKey(finalElement);
|
||||
sqlExpression += ` ->> '${escapedFinalKey}'`;
|
||||
return query.client.raw(sqlExpression);
|
||||
};
|
||||
|
||||
const applyQueryConditionText = (query: Knex.QueryBuilder, { field, tableName, conditions }: QueryConditionText) => {
|
||||
const selector = getFieldSelector(query, field, tableName);
|
||||
if (conditions.equal) {
|
||||
query = query.where(selector, '=', conditions.equal);
|
||||
}
|
||||
if (conditions.notEqual) {
|
||||
query = query.where(selector, '<>', conditions.notEqual);
|
||||
}
|
||||
if (conditions.like) {
|
||||
query = query.whereLike(selector, conditions.like);
|
||||
}
|
||||
if (conditions.notLike) {
|
||||
query = query.not.whereLike(selector, conditions.notLike);
|
||||
}
|
||||
if (conditions.equal === null) {
|
||||
query = query.whereNull(selector);
|
||||
}
|
||||
if (conditions.notEqual === null) {
|
||||
query = query.whereNotNull(selector);
|
||||
}
|
||||
if (conditions.in) {
|
||||
query = query.whereIn(selector, conditions.in);
|
||||
}
|
||||
if (conditions.notIn) {
|
||||
query = query.whereNotIn(selector, conditions.notIn);
|
||||
}
|
||||
return query;
|
||||
};
|
||||
|
||||
const applyQueryConditionNumber = (
|
||||
query: Knex.QueryBuilder,
|
||||
{ field, tableName, conditions }: QueryConditionNumber,
|
||||
) => {
|
||||
const selector = getFieldSelector(query, field, tableName);
|
||||
if (conditions.equals !== undefined && conditions.equals !== null) {
|
||||
query = query.where(selector, '=', conditions.equals);
|
||||
}
|
||||
if (conditions.notEquals !== undefined && conditions.notEquals !== null) {
|
||||
query = query.where(selector, '<>', conditions.notEquals);
|
||||
}
|
||||
if (conditions.equals === null) {
|
||||
query = query.whereNull(selector);
|
||||
}
|
||||
if (conditions.notEquals === null) {
|
||||
query = query.whereNotNull(selector);
|
||||
}
|
||||
if (conditions.greaterThan) {
|
||||
query = query.where(selector, '>', conditions.greaterThan);
|
||||
}
|
||||
if (conditions.greaterThanOrEqual) {
|
||||
query = query.where(selector, '>=', conditions.greaterThanOrEqual);
|
||||
}
|
||||
if (conditions.lessThan) {
|
||||
query = query.where(selector, '<', conditions.lessThan);
|
||||
}
|
||||
if (conditions.lessThanOrEqual) {
|
||||
query = query.where(selector, '<=', conditions.lessThanOrEqual);
|
||||
}
|
||||
if (conditions.in) {
|
||||
query = query.whereIn(selector, conditions.in);
|
||||
}
|
||||
if (conditions.notIn) {
|
||||
query = query.whereNotIn(selector, conditions.notIn);
|
||||
}
|
||||
return query;
|
||||
};
|
||||
|
||||
const applyQueryCondition = (query: Knex.QueryBuilder, options: QueryCondition) => {
|
||||
switch (options.type) {
|
||||
case 'text': {
|
||||
return applyQueryConditionText(query, options);
|
||||
}
|
||||
case 'number': {
|
||||
return applyQueryConditionNumber(query, options);
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown filter type`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const applyQueryFilter = (query: Knex.QueryBuilder, filter: QueryFilter) => {
|
||||
if (filter.type === 'operator') {
|
||||
if (filter.conditions.length === 0) {
|
||||
return query;
|
||||
}
|
||||
|
||||
switch (filter.operator) {
|
||||
case 'or': {
|
||||
return query.where((subquery) => {
|
||||
let isFirst = true;
|
||||
for (const condition of filter.conditions) {
|
||||
if (isFirst) {
|
||||
applyQueryFilter(subquery, condition);
|
||||
isFirst = false;
|
||||
} else {
|
||||
subquery.orWhere((subSubquery) => {
|
||||
applyQueryFilter(subSubquery, condition);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
case 'and': {
|
||||
return query.where((subquery) => {
|
||||
let isFirst = true;
|
||||
for (const condition of filter.conditions) {
|
||||
if (isFirst) {
|
||||
applyQueryFilter(subquery, condition);
|
||||
isFirst = false;
|
||||
} else {
|
||||
subquery.andWhere((subSubquery) => {
|
||||
applyQueryFilter(subSubquery, condition);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return applyQueryCondition(query, filter);
|
||||
}
|
||||
};
|
||||
|
||||
export { applyQueryCondition, applyQueryFilter };
|
||||
8
packages/runtime/src/utils/utils.schema.ts
Normal file
8
packages/runtime/src/utils/utils.schema.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { z, type ZodType } from 'zod';
|
||||
|
||||
const createListResultSchema = <T extends ZodType>(schema: T) =>
|
||||
z.object({
|
||||
items: z.array(schema),
|
||||
});
|
||||
|
||||
export { createListResultSchema };
|
||||
51
packages/runtime/src/utils/utils.services.ts
Normal file
51
packages/runtime/src/utils/utils.services.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
const destroy = Symbol('destroy');
|
||||
const instanceKey = Symbol('instances');
|
||||
|
||||
type ServiceDependency<T> = new (services: Services) => T & {
|
||||
[destroy]?: () => Promise<void> | void;
|
||||
};
|
||||
|
||||
class Services {
|
||||
[instanceKey]: Map<ServiceDependency<unknown>, unknown>;
|
||||
|
||||
constructor() {
|
||||
this[instanceKey] = new Map();
|
||||
}
|
||||
|
||||
public get = <T>(service: ServiceDependency<T>) => {
|
||||
if (!this[instanceKey].has(service)) {
|
||||
this[instanceKey].set(service, new service(this));
|
||||
}
|
||||
const instance = this[instanceKey].get(service);
|
||||
if (!instance) {
|
||||
throw new Error('Could not generate instance');
|
||||
}
|
||||
return instance as T;
|
||||
};
|
||||
|
||||
public set = <T>(service: ServiceDependency<T>, instance: Partial<T>) => {
|
||||
this[instanceKey].set(service, instance);
|
||||
};
|
||||
|
||||
public clone = () => {
|
||||
const services = new Services();
|
||||
services[instanceKey] = Object.fromEntries(this[instanceKey].entries());
|
||||
};
|
||||
|
||||
public destroy = async () => {
|
||||
await Promise.all(
|
||||
this[instanceKey].values().map(async (instance) => {
|
||||
if (
|
||||
typeof instance === 'object' &&
|
||||
instance &&
|
||||
destroy in instance &&
|
||||
typeof instance[destroy] === 'function'
|
||||
) {
|
||||
await instance[destroy]();
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export { Services, destroy };
|
||||
Reference in New Issue
Block a user