import { CancelToken, IDisposable } from "@web-atoms/core/dist/core/types"; import DateTime from "@web-atoms/date-time/dist/DateTime"; import { Cloner } from "../models/Cloner"; import IClrEntity, { IClrEntityLike, IClrExtendedEntity } from "../models/IClrEntity"; import IEntityModel, { EntityContext } from "../models/IEntityModel"; import HttpSession, { IHttpRequest } from "./HttpSession"; import mergeProperties from "./mergeProperties"; import Query, { IDateRange, IEntityWithDateRange, stepTypes } from "./Query"; import resolve from "./resolve"; import { QueryProcessor } from "./QueryProcessor"; export interface IGeometry { latitude: number; longitude: number; wktString?: string; difference?(g: IGeometry): IGeometry; intersection?(g: IGeometry): IGeometry; union?(g: IGeometry): IGeometry; symmetricDifference?(g: IGeometry): IGeometry; distance?(g: IGeometry): number; isWithinDistance?(g: IGeometry, distance: number): boolean; touches?(g: IGeometry): boolean; intersects?(g: IGeometry): boolean; crosses?(g: IGeometry): boolean; within?(g: IGeometry): boolean; contains?(g: IGeometry): boolean; overlaps?(g: IGeometry): boolean; covers?(g: IGeometry): boolean; coveredBy?(g: IGeometry): boolean; } export interface IKeyCollection extends ICollection { key?: TKey; } export interface ICollection extends Array { sum?(filter?: (item: T) => number): number; min?(filter?: (item: T) => number): number; max?(filter?: (item: T) => number): number; average?(filter?: (item: T) => number): number; groupBy?(this: ICollection, selector: (item: T) => TK): ICollection>; where?(filter: (item: T) => boolean): ICollection; any?(filter?: (item: T) => boolean): boolean; select?(select: (item: T) => TR): ICollection; selectMany?(select: (item: T) => TR[]): ICollection; firstOrDefault?(filter?: (item: T) => boolean): T; count?(filter?: (item: T) => boolean): number; toArray?(): ICollection; toList?(): ICollection; take?(n: number): ICollection; orderBy?(item: (item: T) => any): ICollection; thenBy?(item: (item: T) => any): ICollection; orderByDescending?(item: (item: T) => any): ICollection; thenByDescending?(item: (item: T) => any): ICollection; } const ArrayPrototype = Array.prototype as any; ArrayPrototype.where = ArrayPrototype.filter; ArrayPrototype.any = ArrayPrototype.some; ArrayPrototype.select = ArrayPrototype.map; ArrayPrototype.selectMany = function(x) { const r = []; for (const iterator of this) { const items = x(iterator); if (Array.isArray(items)) { r.push(... items); } } return r; }; ArrayPrototype.firstOrDefault = function(f) { if (f) { return ArrayPrototype.find.apply(this, arguments); } return this[0]; }; ArrayPrototype.sum = function(f) { let n = 0; for (const iterator of this) { n += f(iterator) ?? 0; } return n; }; ArrayPrototype.average = function(f) { if (this.length === 0) { return 0; } let n = 0; for (const iterator of this) { n += f(iterator) ?? 0; } return n / this.length; }; ArrayPrototype.orderBy = function(f) { return this.sort((a, b) => { const ak = f(a); const bk = f(b); if (typeof ak === "string") { return ak.toLowerCase().localeCompare((bk as string).toLowerCase()); } return ak - bk; }); }; ArrayPrototype.thenBy = ArrayPrototype.orderBy; ArrayPrototype.orderByDescending = function(f) { return this.sort((a, b) => { const ak = f(a); const bk = f(b); if (typeof ak === "string") { return bk.toLowerCase().localeCompare((ak as string).toLowerCase()); } return bk - ak; }); }; ArrayPrototype.thenByDescending = ArrayPrototype.orderByDescending; ArrayPrototype.count = function(f) { if (!f) { return this.length; } let length = 0; for (const iterator of this) { if (f(iterator)) { length++; } } return length; }; export interface IMethod { select?: [string, ... any[]]; where?: [string, ... any[]]; orderBy?: [string, ... any[]]; orderByDescending?: [string, ... any[]]; thenBy?: [string, ... any[]]; thenByDescending?: [string, ... any[]]; } export interface IMethodsFilter { methods: IMethod[]; start: number; size: number; } export interface IModifications { [key: string]: any; } export interface IBulkUpdateModel { keys: IClrEntity[]; update: IModifications; throwWhenNotFound?: boolean; } export interface IBulkDeleteModel { keys: IClrEntity[]; throwWhenNotFound?: boolean; } export type IQueryMethod = ["select", string, ... any[]] | ["where", string, ... any[]] | ["joinDateRange", string, ... any[]] | ["orderBy", string, ... any[]] | ["orderByDescending", string, ... any[]] | ["thenBy", string, ... any[]] | ["thenByDescending", string, ... any[]] | ["include", string] | ["thenInclude", string] | ["dateRange", string, ... any[]]; export interface IListParams { cancelToken?: CancelToken; /** * Query will resolve references by replacing $id attributed objects */ doNotResolve?: boolean; /** * Do not display activity indicator */ hideActivityIndicator?: boolean; /** * Response will include cache-control with given seconds as max age */ cacheSeconds?: number; /** * Arbitrary cache version to invalidate previous version */ cacheVersion?: string; /** * True if cacheSeconds is greater than zero, set false to turn it off */ cacheImmutable?: boolean; /** * Split server side includes */ splitInclude?: boolean; } export interface IPagedListParams extends IListParams { start?: number; size?: number; count?: boolean; } export interface IModel { name: string; create?(properties?: IClrEntityLike): T; patch?(original: IClrEntityLike, updates: IClrEntityLike): T; } export class DefaultFactory { constructor(public readonly factory: () => any) {} } export class Model implements IModel { private defaults: [string, any][]; constructor( public name: string, public readonly keys: string[] = [], defaults: any = null ) { if (defaults) { this.defaults = []; for (const key in defaults) { if (Object.prototype.hasOwnProperty.call(defaults, key)) { const element = defaults[key]; this.defaults.push([key, element]); } } } } public create(properties: IClrEntityLike = {} as any): T { (properties as any).$type = this.name; if (this.defaults) { for (const [key, value] of this.defaults) { if (properties[key] === void 0) { if (value instanceof DefaultFactory) { properties[key] = value.factory(); } else { properties[key] = value; } } } } return properties as T; } public patch(original: IClrEntityLike, updates: IClrEntityLike) { for (const iterator of this.keys) { const originalKey = original[iterator]; const updatedKey = updates[iterator]; if (updatedKey && updatedKey !== originalKey) { throw new Error(`Cannot update ${iterator} as it is the primary key`) } updates[iterator] = originalKey; } return { $type: this.name, ... updates } as T; } } export default abstract class BaseEntityService extends HttpSession { public url: string = "/api/entity/"; public abstract queryProcessor: QueryProcessor; protected resultConverter = resolve; private entityModel: EntityContext; public cloner(item: T): Cloner { return new Cloner(item); } public async model(): Promise { if (this.entityModel) { return this.entityModel; } const c = await this.getJson({ url: `${this.url}model` }); this.entityModel = new EntityContext(c); return this.entityModel; } public dateRange(start: DateTime, end: DateTime, step: stepTypes ): Query { return new Query({ service: this, name: "NeuroSpeech.EntityAccessControl.DateRange", traceQuery: false}, [ ["dateRange", "@0,@1,@2", start, end, step] ]); } public query(m: IModel, queryFunction?: string, ... args: any[]): Query { return new Query({ service: this, name: m.name, queryProcessor: this.queryProcessor, queryFunction, args }); } public delete(body: T): Promise { const url = this.url; return this.deleteJson({url, body}); } public insert(body: IClrEntity): Promise { const url = this.url; return this.putJson({url, body}); } public save(body: T): Promise; public save(body: T[]): Promise; public async save(body: any): Promise { if (Array.isArray(body) && body.length === 0) { return body; } const url = this.url; const result = await this.postJson({url, body}); mergeProperties(result, body); return body; } public async update(e: T, update: IModifications): Promise { await this.bulkUpdate([e], update); for (const key in update) { if (Object.prototype.hasOwnProperty.call(update, key)) { const element = update[key]; e[key] = element; } } return e; } public async bulkUpdate( entities: T[], update: IModifications, throwWhenNotFound: boolean = false): Promise { const model = await this.model(); const keys = []; for (const iterator of entities) { const entityType = model.for(iterator.$type); const key = { $type: iterator.$type }; for (const { name } of entityType.keys) { key[name] = iterator[name]; } keys.push(key); } const body = { keys, update, throwWhenNotFound }; const url = `${this.url}bulk`; await this.putJson({url, body}); } public async bulkDelete( entities: T[], throwWhenNotFound: boolean = false): Promise { const model = await this.model(); const keys = []; for (const iterator of entities) { const entityType = model.for(iterator.$type); const key = { $type: iterator.$type }; for (const { name } of entityType.keys) { key[name] = iterator[name]; } keys.push(key); } const url = `${this.url}bulk`; const body = { keys, throwWhenNotFound }; await this.deleteJson({ url, body }); } protected async fetchJson(options: IHttpRequest): Promise { if (!this.createBusyIndicator || options?.hideActivityIndicator) { return await super.fetchJson(options); } const disposable = this.createBusyIndicator(options); try { return await super.fetchJson(options); } finally { disposable?.dispose(); } } protected createBusyIndicator(options: IHttpRequest) { return { dispose() {}}; } } // @ts-expect-error delete BaseEntityService.prototype.createBusyIndicator;