From 3b07300bfd5ba96636d4ccbde692a7646412e872 Mon Sep 17 00:00:00 2001 From: Ivan S Glazunov Date: Sat, 13 Jan 2024 13:53:29 +0000 Subject: [PATCH] minilinks strings id support and cyber boilerplayt --- imports/cyber.tsx | 1022 ++++++++++++++++++++++++++++++++++++ imports/minilinks-query.ts | 18 +- imports/minilinks.ts | 86 +-- 3 files changed, 1075 insertions(+), 51 deletions(-) create mode 100644 imports/cyber.tsx diff --git a/imports/cyber.tsx b/imports/cyber.tsx new file mode 100644 index 00000000..34beb947 --- /dev/null +++ b/imports/cyber.tsx @@ -0,0 +1,1022 @@ +import atob from 'atob'; +import { gql, useQuery, useSubscription, useApolloClient, Observable } from '@apollo/client/index.js'; +import type { ApolloQueryResult } from '@apollo/client/index.js'; +import { generateApolloClient, IApolloClient } from '@deep-foundation/hasura/client.js'; +import { useLocalStore } from '@deep-foundation/store/local.js'; +import React, { createContext, useContext, useEffect, useMemo, useState } from "react"; +import { deprecate, inherits, inspect } from "util"; +import { deleteMutation, generateMutation, generateQuery, generateQueryData, generateSerial, IGenerateMutationBuilder, IGenerateMutationOptions, insertMutation, ISerialResult, updateMutation } from './gql/index.js'; +import { Link, MinilinkCollection, minilinks, MinilinksInstance, MinilinksResult, useMinilinksApply, useMinilinksQuery, useMinilinksSubscription } from './minilinks.js'; +import { awaitPromise } from './promise.js'; +import { useTokenController } from './react-token.js'; +import { reserve } from './reserve.js'; +import { corePckg } from './core.js'; +import { BoolExpCan, BoolExpHandler, QueryLink, BoolExpSelector, BoolExpTree, BoolExpValue, MutationInputLink, MutationInputLinkPlain, MutationInputValue } from './client_types.js'; +import get from 'get-value'; +import {debug} from './debug.js' +import { Traveler as NativeTraveler } from './traveler.js'; +const moduleLog = debug.extend('client'); + +import { + Subscription, + Observer, + DeepClientOptions, DeepClientResult, DeepClientPackageSelector, DeepClientPackageContain, DeepClientLinkId, DeepClientStartItem, DeepClientPathItem, + _serialize, _ids, _boolExpFields, pathToWhere, serializeWhere, serializeQuery, parseJwt +} from './client.js'; + +const log = debug.extend('log'); +const error = debug.extend('error'); + +const corePckgIds: { [key: string]: number; } = {}; +corePckg.data.filter(l => !!l.type).forEach((l, i) => { + corePckgIds[l.id] = i+1; +}); + +export interface DeepClientInstance = Link> { + linkId?: number; + token?: string; + handleAuth?: (linkId?: number, token?: string) => any; + + deep: DeepClientInstance; + + apolloClient: IApolloClient; + minilinks: MinilinksResult; + table?: string; + returning?: string; + + selectReturning?: string; + linksSelectReturning?: string; + valuesSelectReturning?: string; + selectorsSelectReturning?: string; + filesSelectReturning?: string; + insertReturning?: string; + updateReturning?: string; + deleteReturning?: string; + + defaultSelectName?: string; + defaultInsertName?: string; + defaultUpdateName?: string; + defaultDeleteName?: string; + + unsafe?: any; + + stringify(any?: any): string; + + select(exp: Exp, options?: ReadOptions): Promise>; + subscribe(exp: Exp, options?: ReadOptions): Observable; + + insert(objects: InsertObjects , options?: WriteOptions):Promise>; + + update(exp: Exp, value: UpdateValue, options?: WriteOptions):Promise>; + + delete(exp: Exp, options?: WriteOptions):Promise>; + + serial(options: AsyncSerialParams): Promise>; + + reserve(count: number): Promise; + + await(id: number): Promise; + + + serializeWhere(exp: any, env?: string): any; + serializeQuery(exp: any, env?: string): any; + + id(start: DeepClientStartItem | QueryLink, ...path: DeepClientPathItem[]): Promise; + idLocal(start: DeepClientStartItem, ...path: DeepClientPathItem[]): number; + + guest(options: DeepClientGuestOptions): Promise; + + jwt(options: DeepClientJWTOptions): Promise; + + login(options: DeepClientJWTOptions): Promise; + + logout(): Promise; + + can(objectIds: number[], subjectIds: number[], actionIds: number[]): Promise; + + useDeepSubscription: typeof useDeepSubscription; + useDeepQuery: typeof useDeepQuery; + useMinilinksQuery: (query: QueryLink) => L[]; + useMinilinksSubscription: (query: QueryLink) => L[]; + useDeep: typeof useDeep; + DeepProvider: typeof DeepProvider; + DeepContext: typeof DeepContext; + + Traveler(links: Link[]): NativeTraveler; +} + +export interface DeepClientAuthResult { + linkId?: number; + token?: string; + error?: any; +} + +export interface DeepClientGuestOptions { + relogin?: boolean; +} + +export interface DeepClientJWTOptions { + linkId?: number; + token?: string; + relogin?: boolean; +} + + +export type SelectTable = 'links' | 'numbers' | 'strings' | 'objects' | 'can' | 'selectors' | 'tree' | 'handlers'; +export type InsertTable = 'links' | 'numbers' | 'strings' | 'objects'; +export type UpdateTable = 'links' | 'numbers' | 'strings' | 'objects' | 'can' | 'selectors' | 'tree' | 'handlers'; +export type DeleteTable = 'links' | 'numbers' | 'strings' | 'objects' | 'can' | 'selectors' | 'tree' | 'handlers'; + +export type OperationType = 'select' | 'insert' | 'update' | 'delete'; +export type SerialOperationType = 'insert' | 'update' | 'delete'; +export type Table = TOperationType extends 'select' + ? SelectTable + : TOperationType extends 'insert' + ? InsertTable + : TOperationType extends 'update' + ? UpdateTable + : TOperationType extends 'delete' + ? DeleteTable + : never; + +export type ValueForTable = TTable extends 'numbers' + ? MutationInputValue + : TTable extends 'strings' + ? MutationInputValue + : TTable extends 'objects' + ? MutationInputValue + : MutationInputLink; + +export type ExpForTable = TTable extends 'numbers' + ? BoolExpValue + : TTable extends 'strings' + ? BoolExpValue + : TTable extends 'objects' + ? BoolExpValue + : TTable extends 'can' + ? BoolExpCan + : TTable extends 'selectors' + ? BoolExpSelector + : TTable extends 'tree' + ? BoolExpTree + : TTable extends 'handlers' + ? BoolExpHandler + : QueryLink; + +export type SerialOperationDetails< + TSerialOperationType extends SerialOperationType, + TTable extends Table +> = TSerialOperationType extends 'insert' + ? { + objects: ValueForTable | ValueForTable[]; + } + : TSerialOperationType extends 'update' + ? { + exp: ExpForTable | number | number[]; + value: ValueForTable; + } + : TSerialOperationType extends 'delete' + ? { + exp: ExpForTable | number | number[]; + } + : never; + +export type SerialOperation< + TSerialOperationType extends SerialOperationType = SerialOperationType, + TTable extends Table = Table, +> = { + type: TSerialOperationType; + table: TTable; +} & SerialOperationDetails; + +export type DeepSerialOperation = SerialOperation> + +export type AsyncSerialParams = { + operations: Array; + name?: string; + returning?: string; + silent?: boolean; +}; + +export function checkAndFillShorts(obj) { + for (var i in obj) { + if (!obj.hasOwnProperty(i)) continue; + if ((typeof obj[i]) == 'object' && obj[i] !== null) { + if (typeof obj[i] === 'object' && i === 'object' && obj[i]?.data?.value === undefined) { obj[i] = { data: { value: obj[i] } }; continue; } + if (typeof obj[i] === 'object' && (i === 'to' || i === 'from' || i === 'in' || i === 'out') && obj[i]?.data === undefined) obj[i] = { data: obj[i] }; + checkAndFillShorts(obj[i]); + } + else if (i === 'string' && typeof obj[i] === 'string' || i === 'number' && typeof obj[i] === 'number') obj[i] = { data: { value: obj[i] } }; + } +} + +export class DeepClient = Link> implements DeepClientInstance { + static resolveDependency?: (path: string) => Promise + + useDeepSubscription = useDeepSubscription; + useDeepQuery = useDeepQuery; + useMinilinksQuery = (query: QueryLink) => useMinilinksQuery(this.minilinks, query); + useMinilinksSubscription = (query: QueryLink) => useMinilinksSubscription(this.minilinks, query) + useDeep = useDeep; + DeepProvider = DeepProvider; + DeepContext = DeepContext; + + linkId?: number; + token?: string; + handleAuth?: (linkId?: number, token?: string) => any; + + deep: DeepClientInstance; + + client: IApolloClient; + apolloClient: IApolloClient; + minilinks: MinilinksResult; + table?: string; + returning?: string; + + selectReturning?: string; + linksSelectReturning?: string; + valuesSelectReturning?: string; + selectorsSelectReturning?: string; + filesSelectReturning?: string; + insertReturning?: string; + updateReturning?: string; + deleteReturning?: string; + + defaultSelectName?: string; + defaultInsertName?: string; + defaultUpdateName?: string; + defaultDeleteName?: string; + + silent: boolean; + + unsafe?: any; + + _silent(options: Partial<{ silent?: boolean }> = {}): boolean { + return typeof(options.silent) === 'boolean' ? options.silent : this.silent; + } + + constructor(options: DeepClientOptions) { + this.deep = options.deep; + if (!this.apolloClient) this.apolloClient = options.apolloClient; + + if (!this.deep && !options.apolloClient) throw new Error('options.apolloClient or options.deep is required'); + + if (this.deep && !this.apolloClient && !options.apolloClient && options.token) { + this.apolloClient = generateApolloClient({ + // @ts-ignore + path: this.deep.apolloClient?.path, + // @ts-ignore + ssl: this.deep.apolloClient?.ssl, + token: options.token, + }); + } + + if (!this.apolloClient) throw new Error('apolloClient is invalid'); + + this.client = this.apolloClient; + + // @ts-ignore + this.minilinks = options.minilinks || new MinilinkCollection(); + this.table = options.table || 'links'; + + this.token = options.token; + + if (this.token) { + const decoded = parseJwt(this.token); + const linkId = decoded?.userId; + if (!linkId){ + throw new Error(`Unable to parse linkId from jwt token.`); + } + if (options.linkId && options.linkId !== linkId){ + throw new Error(`linkId (${linkId}) parsed from jwt token is not the same as linkId passed via options (${options.linkId}).`); + } + this.linkId = linkId; + } else { + this.linkId = options.linkId; + } + + this.handleAuth = options?.handleAuth || options?.deep?.handleAuth; + + this.linksSelectReturning = options.linksSelectReturning || options.selectReturning || 'id type_id from_id to_id value'; + this.selectReturning = options.selectReturning || this.linksSelectReturning; + this.valuesSelectReturning = options.valuesSelectReturning || 'id link_id value'; + this.selectorsSelectReturning = options.selectorsSelectReturning ||'item_id selector_id'; + this.filesSelectReturning = options.filesSelectReturning ||'id link_id name mimeType'; + this.insertReturning = options.insertReturning || 'id'; + this.updateReturning = options.updateReturning || 'id'; + this.deleteReturning = options.deleteReturning || 'id'; + + this.defaultSelectName = options.defaultSelectName || 'SELECT'; + this.defaultInsertName = options.defaultInsertName || 'INSERT'; + this.defaultUpdateName = options.defaultUpdateName || 'UPDATE'; + this.defaultDeleteName = options.defaultDeleteName || 'DELETE'; + + this.silent = options.silent || false; + + this.unsafe = options.unsafe || {}; + } + + stringify(any?: any): string { + if (typeof(any) === 'string') { + let json; + try { json = JSON.parse(any); } catch(e) {} + return json ? JSON.stringify(json, null, 2) : any.toString(); + } else if (typeof(any) === 'object') { + return JSON.stringify(any, null, 2); + } + return any; + } + + serializeQuery = serializeQuery; + serializeWhere = serializeWhere; + + async select(exp: Exp, options?: ReadOptions): Promise> { + if (!exp) { + return { error: { message: '!exp' }, data: undefined, loading: false, networkStatus: undefined }; + } + const query = serializeQuery(exp, options?.table || 'links'); + const table = options?.table || this.table; + const returning = options?.returning ?? + (table === 'links' ? this.linksSelectReturning : + ['strings', 'numbers', 'objects'].includes(table) ? this.valuesSelectReturning : + table === 'selectors' ? this.selectorsSelectReturning : + table === 'files' ? this.filesSelectReturning : `id`); + const tableNamePostfix = options?.tableNamePostfix; + const aggregate = options?.aggregate; + + // console.log(`returning: ${returning}; options.returning:${options?.returning}`) + const variables = options?.variables; + const name = options?.name || this.defaultSelectName; + + try { + const q = await this.apolloClient.query(generateQuery({ + queries: [ + generateQueryData({ + tableName: table, + tableNamePostfix: tableNamePostfix || aggregate ? '_aggregate' : '', + returning: aggregate ? `aggregate { ${aggregate} }` : returning, + variables: { + ...variables, + ...query, + } }), + ], + name: name, + })); + + return { ...q, data: aggregate ? (q)?.data?.q0?.aggregate?.[aggregate] : (q)?.data?.q0 }; + } catch (e) { + console.log(generateQueryData({ + tableName: table, + tableNamePostfix: tableNamePostfix || aggregate ? '_aggregate' : '', + returning: aggregate ? `aggregate { ${aggregate} }` : returning, + variables: { + ...variables, + ...query, + } })('a', 0)); + throw new Error(`DeepClient Select Error: ${e.message}`, { cause: e }); + } + }; + + /** + * deep.subscribe + * @example + * deep.subscribe({ up: { link_id: 380 } }).subscribe({ next: (links) => {}, error: (err) => {} }); + */ + subscribe(exp: Exp, options?: ReadOptions): Observable { + if (!exp) { + return new Observable((observer) => { + observer.error('!exp'); + }); + } + const query = serializeQuery(exp, options?.table || 'links'); + const table = options?.table || this.table; + const returning = options?.returning ?? + (table === 'links' ? this.linksSelectReturning : + ['strings', 'numbers', 'objects'].includes(table) ? this.valuesSelectReturning : + table === 'selectors' ? this.selectorsSelectReturning : + table === 'files' ? this.filesSelectReturning : `id`); + const tableNamePostfix = options?.tableNamePostfix; + const aggregate = options?.aggregate; + + // console.log(`returning: ${returning}; options.returning:${options?.returning}`) + const variables = options?.variables; + const name = options?.name || this.defaultSelectName; + + try { + const apolloObservable = this.apolloClient.subscribe({ + ...generateQuery({ + operation: 'subscription', + queries: [ + generateQueryData({ + tableName: table, + tableNamePostfix: tableNamePostfix || aggregate ? '_aggregate' : '', + returning: returning || aggregate ? `aggregate { ${aggregate} }` : returning, + variables: { + ...variables, + ...query, + } }), + ], + name: name, + }), + }); + + const observable = new Observable((observer) => { + const subscription = apolloObservable.subscribe({ + next: (data: any) => { + observer.next(aggregate ? data?.q0?.aggregate?.[aggregate] : data?.q0); + }, + error: (error) => observer.error(error), + }); + return () => subscription.unsubscribe(); + }); + + // @ts-ignore + return observable; + } catch (e) { + throw new Error(`DeepClient Subscription Error: ${e.message}`, { cause: e }); + } + }; + + async insert(objects: InsertObjects, options?: WriteOptions):Promise> { + const _objects = Object.prototype.toString.call(objects) === '[object Array]' ? objects : [objects]; + checkAndFillShorts(_objects); + + const table = options?.table || this.table; + const returning = options?.returning || this.insertReturning; + const variables = options?.variables; + const name = options?.name || this.defaultInsertName; + let q: any = {}; + + try { + q = await this.apolloClient.mutate(generateSerial({ + actions: [insertMutation(table, { ...variables, objects: _objects }, { tableName: table, operation: 'insert', returning })], + name: name, + })); + } catch(e) { + const sqlError = e?.graphQLErrors?.[0]?.extensions?.internal?.error; + if (sqlError?.message) e.message = sqlError.message; + if (!this._silent(options)) throw new Error(`DeepClient Insert Error: ${e.message}`, { cause: e }) + return { ...q, data: (q)?.data?.m0?.returning, error: e }; + } + + // @ts-ignore + return { ...q, data: (q)?.data?.m0?.returning }; + }; + + async update(exp: Exp, value: UpdateValue, options?: WriteOptions):Promise> { + if (exp === null) return this.insert( [value], options); + if (value === null) return this.delete( exp, options ); + + const query = serializeQuery(exp, options?.table === this.table || !options?.table ? 'links' : 'value'); + const table = options?.table || this.table; + const returning = options?.returning || this.updateReturning; + const variables = options?.variables; + const name = options?.name || this.defaultUpdateName; + let q; + try { + q = await this.apolloClient.mutate(generateSerial({ + actions: [updateMutation(table, { ...variables, ...query, _set: value }, { tableName: table, operation: 'update', returning })], + name: name, + })); + } catch(e) { + const sqlError = e?.graphQLErrors?.[0]?.extensions?.internal?.error; + if (sqlError?.message) e.message = sqlError.message; + if (!this._silent(options)) throw new Error(`DeepClient Update Error: ${e.message}`, { cause: e }); + return { ...q, data: (q)?.data?.m0?.returning, error: e }; + } + // @ts-ignore + return { ...q, data: (q)?.data?.m0?.returning }; + }; + + async delete(exp: Exp, options?: WriteOptions):Promise> { + if (!exp) throw new Error('!exp'); + const query = serializeQuery(exp, options?.table === this.table || !options?.table ? 'links' : 'value'); + const table = options?.table || this.table; + const returning = options?.returning || this.deleteReturning; + const variables = options?.variables; + const name = options?.name || this.defaultDeleteName; + let q; + try { + q = await this.apolloClient.mutate(generateSerial({ + actions: [deleteMutation(table, { ...variables, ...query, returning }, { tableName: table, operation: 'delete', returning })], + name: name, + })); + // @ts-ignore + } catch(e) { + const sqlError = e?.graphQLErrors?.[0]?.extensions?.internal?.error; + if (sqlError?.message) e.message = sqlError.message; + if (!this._silent(options)) throw new Error(`DeepClient Delete Error: ${e.message}`, { cause: e }); + return { ...q, data: (q)?.data?.m0?.returning, error: e }; + } + return { ...q, data: (q)?.data?.m0?.returning }; + }; + + async serial({ + name, operations, returning, silent + }: AsyncSerialParams): Promise> { + // @ts-ignore + let operationsGroupedByTypeAndTable: Record>> = {}; + operationsGroupedByTypeAndTable = operations.reduce((acc, operation) => { + if (!acc[operation.type]) { + // @ts-ignore + acc[operation.type] = {} + } + if (!acc[operation.type][operation.table]) { + acc[operation.type][operation.table] = [] + } + acc[operation.type][operation.table].push(operation); + return acc + }, operationsGroupedByTypeAndTable); + let serialActions: Array = []; + Object.keys(operationsGroupedByTypeAndTable).map((operationType: SerialOperationType) => { + const operationsGroupedByTable = operationsGroupedByTypeAndTable[operationType]; + Object.keys(operationsGroupedByTable).map((table: Table) => { + const operations = operationsGroupedByTable[table]; + if (operationType === 'insert') { + const insertOperations = operations as Array>>; + const serialAction: IGenerateMutationBuilder = insertMutation(table, { objects: insertOperations.map(operation => Array.isArray(operation.objects) ? operation.objects : [operation.objects]).flat() }, { tableName: table, operation: operationType, returning }) + serialActions.push(serialAction); + } else if (operationType === 'update') { + const updateOperations = operations as Array>>; + const newSerialActions: IGenerateMutationBuilder[] = updateOperations.map(operation => { + const exp = operation.exp; + const value = operation.value; + const query = serializeQuery(exp, table === this.table || !table ? 'links' : 'value'); + return updateMutation(table, {...query, _set: value }, { tableName: table, operation: operationType ,returning}) + }) + serialActions = [...serialActions, ...newSerialActions]; + } else if (operationType === 'delete') { + const deleteOperations = operations as Array>>;; + const newSerialActions: IGenerateMutationBuilder[] = deleteOperations.map(operation => { + const exp = operation.exp; + const query = serializeQuery(exp, table === this.table || !table ? 'links' : 'value'); + return deleteMutation(table, { ...query }, { tableName: table, operation: operationType, returning }) + }) + serialActions = [...serialActions, ...newSerialActions]; + } + }) + }) + + let result; + try { + result = await this.apolloClient.mutate(generateSerial({ + actions: serialActions, + name: name ?? 'Name', + })) + // @ts-ignore + } catch (e) { + const sqlError = e?.graphQLErrors?.[0]?.extensions?.internal?.error; + if (sqlError?.message) e.message = sqlError.message; + if (!silent) throw new Error(`DeepClient Serial Error: ${e.message}`, { cause: e }); + return { ...result, data: (result)?.data?.m0?.returning, error: e }; + } + // @ts-ignore + return { ...result, data: (result)?.data && Object.values((result)?.data).flatMap(m => m.returning)}; + }; + + reserve(count: number): Promise { + return reserve({ count, client: this.apolloClient }); + }; + + async await(id: number, options: { results: boolean } = { results: false } ): Promise { + return awaitPromise({ + id, client: this.apolloClient, + Then: await this.id('@deep-foundation/core', 'Then'), + Promise: await this.id('@deep-foundation/core', 'Promise'), + Resolved: await this.id('@deep-foundation/core', 'Resolved'), + Rejected: await this.id('@deep-foundation/core', 'Rejected'), + Results: options.results + }); + }; + + /** + * Find id of link by packageName/id as first argument, and Contain value (name) as path items. + * @description Thows error if id is not found. You can set last argument true, for disable throwing error. + * @returns number + */ + async id(start: DeepClientStartItem | QueryLink, ...path: DeepClientPathItem[]): Promise { + if (typeof(start) === 'object') { + return ((await this.select(start)) as any)?.data?.[0]?.id; + } + if (_ids?.[start]?.[path[0]]) { + return _ids[start][path[0]]; + } + const q = await this.select(pathToWhere(start, ...path)); + if (q.error) { + throw q.error; + } + // @ts-ignore + const result = (q?.data?.[0]?.id | _ids?.[start]?.[path?.[0]] | 0); + if (!result && path[path.length - 1] !== true) { + throw new Error(`Id not found by [${JSON.stringify([start, ...path])}]`); + } + return result; + }; + + /** + * This function fetches the corresponding IDs from the Deep for each specified path. + * + * @async + * @function ids + * @param {Array<[DeepClientStartItem, ...DeepClientPathItem[]]>} paths - An array of [start, ...path] tuples. + * Each tuple specifies a path to a link, where 'start' is the package name or id + * and ...path further specifies the path to the id using Contain link values (names). + * + * @returns {Promise} - Returns a Promise that resolves to an object. + * The object has keys corresponding to the package name or id of each path. + * The value for each package key is an object where keys are the items in the corresponding path, + * and the values are the IDs retrieved from the Deep. + * + * @throws Will throw an error if the id retrieval fails in `this.id()` function. + * + * @example + * ```ts + * const ids = await deep.ids([ + * ['@deep-foundation/core', 'Package'], + * ['@deep-foundation/core', 'PackageVersion'] + * ]); + * + * // Outputs + * // { + * // "@deep-foundation/core": { + * // "Package": 2, + * // "PackageVersion": 46 + * // } + * // } + * ``` + */ + async ids(...paths: Array<[DeepClientStartItem, ...DeepClientPathItem[]]>): Promise { + // TODO: it can be faster using a combiniation of simple select of packages and contains with specified names and recombination of these links in minilinks + + // At the moment it may be slow, but it demonstrates desired API. + + const appendPath = (accumulator, keys, value) => { + const lastKey = keys.pop(); + const lastObject = keys.reduce((obj, key) => obj[key] = obj[key] || {}, accumulator); + lastObject[lastKey] = value; + }; + const result = {}; + await Promise.all(paths.map(async ([start, ...path]) => { + const id = await this.id(start, ...path); + appendPath(result, [start, ...path], id); + })); + return result; + } + + idLocal(start: DeepClientStartItem, ...path: DeepClientPathItem[]): number { + const paths = [start, ...path] as [DeepClientStartItem, ...Array>]; + if (get(_ids, paths.join('.'))) { + return get(_ids, paths.join('.')); + } + + // let result: number; + // if(paths.length === 1) { + + // } else { + // result = paths[0] as number; + // for (let i = 1; i < paths.length; i++) { + // result = this.idLocal(result, paths[i] as Exclude); + // } + // } + + const [link] = this.minilinks.query({ + id: { + _id: paths + } + }) + const result = (link as Link)?.id; + + if(!result) { + throw new Error(`Id not found by ${JSON.stringify([start, ...path])}`); + } else { + return result as number + } + }; + + async guest(options: DeepClientGuestOptions = {}): Promise { + const relogin = typeof(options.relogin) === 'boolean' ? options.relogin : true; + const result = await this.apolloClient.query({ query: GUEST }); + const { linkId, token, error } = result?.data?.guest || {}; + if (!error && !!token && relogin) { + if (this?.handleAuth) setTimeout(() => this?.handleAuth(+linkId, token), 0); + } + return { linkId, token, error: !error && (!linkId || !token) ? 'unexepted' : error }; + }; + + async jwt(options: DeepClientJWTOptions): Promise { + const relogin = typeof(options.relogin) === 'boolean' ? options.relogin : false; + if (options?.token) { + try { + const token = options?.token; + const decoded = parseJwt(token); + const linkId = decoded?.userId; + if (!!token && relogin) { + if (this?.handleAuth) setTimeout(() => this?.handleAuth(+linkId, token), 0); + } + return { linkId, token, error: (!linkId || !token) ? 'unexepted' : undefined }; + } catch(e) { + return { error: e }; + } + } else if (options?.linkId) { + const result = await this.apolloClient.query({ query: JWT, variables: { linkId: +options.linkId } }); + const { linkId, token, error } = result?.data?.jwt || {}; + if (!error && !!token && relogin) { + if (this?.handleAuth) setTimeout(() => this?.handleAuth(+linkId, token), 0); + } + return { linkId, token, error: error ? error : (!linkId) ? 'unexepted' : undefined }; + } else return { error: `linkId or token must be provided` }; + }; + + /** + * Return is of current authorized user linkId. + * Refill client.linkId and return. + */ + async whoami(): Promise { + const result = await this.apolloClient.query({ query: WHOISME }); + this.linkId = result?.data?.jwt?.linkId; + return result?.data?.jwt?.linkId; + } + + async login(options: DeepClientJWTOptions): Promise { + const jwtResult = await this.jwt({ ...options, relogin: true }); + this.token = jwtResult.token; + this.linkId = jwtResult.linkId; + return jwtResult + }; + + async logout(): Promise { + if (this?.handleAuth) setTimeout(() => this?.handleAuth(0, ''), 0); + return { linkId: 0, token: '' }; + }; + + async can(objectIds: null | number | number[], subjectIds: null | number | number[], actionIds: null | number | number[], userIds: number | number[] = this.linkId) { + const where: any = { + }; + if (objectIds) where.object_id = typeof(objectIds) === 'number' ? { _eq: +objectIds } : { _in: objectIds }; + if (subjectIds) where.subject_id = typeof(subjectIds) === 'number' ? { _eq: +subjectIds } : { _in: subjectIds }; + if (actionIds) where.action_id = typeof(actionIds) === 'number' ? { _eq: +actionIds } : { _in: actionIds }; + const result = await this.select(where, { table: 'can', returning: 'rule_id' }); + return !!result?.data?.length; + } + + async name(input: Link | number): Promise { + const id = typeof(input) === 'number' ? input : input.id; + + // if ((this.minilinks.byId[id] as Link)?.type_id === this.idLocal('@deep-foundation/core', 'Package')) return (this.minilinks.byId[id] as Link)?.value?.value; + const {data: [containLink]} = await this.select({ + type_id: { _id: ['@deep-foundation/core', 'Contain'] }, + to_id: id, + }); + if (!containLink?.value?.value) { + const {data: [packageLink]} = await this.select(id); + if (packageLink?.type_id === this.idLocal('@deep-foundation/core', 'Package')) return packageLink?.value?.value; + } + // @ts-ignore + return containLink?.value?.value; + }; + + nameLocal(input: Link | number): string | undefined { + const id = typeof(input) === 'number' ? input : input?.id; + if (!id) return; + // @ts-ignore + if (this.minilinks.byId[id]?.type_id === this.idLocal('@deep-foundation/core', 'Package')) return this.minilinks.byId[id]?.value?.value; + return (this.minilinks.byType[this.idLocal('@deep-foundation/core', 'Contain')]?.find((c: any) => c?.to_id === id) as any)?.value?.value; + } + + async import(path: string) : Promise { + if (typeof DeepClient.resolveDependency !== 'undefined') { + try { + return await DeepClient.resolveDependency(path); + } catch (e) { + console.log(`IGNORED ERROR: Call to DeepClient.resolveDependency is failed with`, e); + } + } + if (typeof require !== 'undefined') { + try { + return await require(path); + } catch (e) { + console.log(`IGNORED ERROR: Call to require is failed with`, e); + } + } + return await import(path); + } + + Traveler(links: Link[]) { + return new NativeTraveler(this, links); + }; +} + +export const JWT = gql`query JWT($linkId: Int) { + jwt(input: {linkId: $linkId}) { + linkId + token + } +}`; + +export const WHOISME = gql`query WHOISME { + jwt(input: {}) { + linkId + } +}`; + +export const GUEST = gql`query GUEST { + guest { + linkId + token + } +}`; + +export function useAuthNode() { + return useLocalStore('use_auth_link_id', 0); +} + +export const DeepContext = createContext>>(undefined); + +export function useDeepGenerator(apolloClientProps?: IApolloClient) { + const log = debug.extend(useDeepGenerator.name) + const apolloClientHook = useApolloClient(); + log({apolloClientHook}) + const apolloClient: IApolloClient = apolloClientProps || apolloClientHook; + log({apolloClient}) + + const [linkId, setLinkId] = useAuthNode(); + log({linkId, setLinkId}) + const [token, setToken] = useTokenController(); + log({token, setToken}) + + const deep = useMemo(() => { + if (!apolloClient?.jwt_token) { + log({ token, apolloClient }); + } + return new DeepClient({ + apolloClient, linkId, token, + handleAuth: (linkId, token) => { + setToken(token); + setLinkId(linkId); + }, + }); + }, [apolloClient]); + log({deep}) + return deep; +} + +export function DeepProvider({ + apolloClient: apolloClientProps, + children, +}: { + apolloClient?: IApolloClient, + children: any; +}) { + const deep = useDeepGenerator(apolloClientProps); + return + {children} + ; +} + +export function useDeep() { + return useContext(DeepContext); +} + +export function useDeepQuery>( + query: QueryLink, + options?: { + table?: Table; + tableNamePostfix?: string; + returning?: string; + variables?: any; + name?: string; + mini?: string; + }, +): { + data?: LL[]; + error?: any; + loading: boolean; +} { + const [miniName] = useState(options?.mini || Math.random().toString(36).slice(2, 7)); + debug('useDeepQuery', miniName, query, options); + const deep = useDeep(); + const wq = useMemo(() => { + const sq = serializeQuery(query); + return generateQuery({ + operation: 'query', + queries: [generateQueryData({ + tableName: 'links', + returning: ` + id type_id from_id to_id value + string { id value } + number { id value } + object { id value } + `, + ...options, + variables: { ...sq, ...options?.variables } + })], + name: options?.name || 'USE_DEEP_QUERY', + }); + }, [query, options]); + const result = useQuery(wq.query, { variables: wq?.variables }); + useMinilinksApply(deep.minilinks, miniName, result?.data?.q0 || []); + const mlResult = deep.useMinilinksSubscription({ id: { _in: result?.data?.q0?.map(l => l.id) } }); + return { + ...result, + data: mlResult, + }; +} + +export function useDeepSubscription
>( + query: QueryLink, + options?: { + table?: Table; + tableNamePostfix?: string; + returning?: string; + variables?: any; + name?: string; + mini?: string; + }, +): UseDeepSubscriptionResult { + const [miniName] = useState(options?.mini || Math.random().toString(36).slice(2, 7)); + debug('useDeepSubscription', miniName, query, options); + const deep = useDeep(); + const wq = useMemo(() => { + const sq = serializeQuery(query); + return generateQuery({ + operation: 'subscription', + queries: [generateQueryData({ + tableName: 'links', + returning: ` + id type_id from_id to_id value + string { id value } + number { id value } + object { id value } + `, + ...options, + variables: { ...sq, ...options?.variables } + })], + name: options?.name || 'USE_DEEP_SUBSCRIPTION', + }); + }, [query, options]); + const result = useSubscription(wq.query, { variables: wq?.variables }); + useMinilinksApply(deep.minilinks, miniName, result?.data?.q0 || []); + const mlResult = useMinilinksSubscription(deep.minilinks,{ id: { _in: result?.data?.q0?.map(l => l.id) } }); + + return { + ...result, + data: mlResult, + }; +} + +export interface UseDeepSubscriptionResult> { + data?: LL[]; + error?: any; + loading: boolean; +} + +export function useDeepId(start: DeepClientStartItem | QueryLink, ...path: DeepClientPathItem[]): { data: number; loading: boolean; error?: any } { + const result = useDeepQuery({ id: { _id: [start, ...path] } }); + return { data: result?.data?.[0]?.id, loading: result?.loading, error: result?.error }; +} + +export type Exp = ( + TTable extends 'numbers' ? BoolExpValue : + TTable extends 'strings' ? BoolExpValue : + TTable extends 'objects' ? BoolExpValue : + TTable extends 'can' ? BoolExpCan : + TTable extends 'selectors' ? BoolExpSelector : + TTable extends 'tree' ? BoolExpTree : + TTable extends 'handlers' ? BoolExpHandler : + QueryLink +) | number | number[]; + +export type UpdateValue = ( + TTable extends 'numbers' ? MutationInputValue : + TTable extends 'strings' ? MutationInputValue : + TTable extends 'objects' ? MutationInputValue : + MutationInputLinkPlain +); + +export type InsertObjects = ( + TTable extends 'numbers' ? MutationInputValue : + TTable extends 'strings' ? MutationInputValue : + TTable extends 'objects' ? MutationInputValue : + MutationInputLink +) | ( + TTable extends 'numbers' ? MutationInputValue : + TTable extends 'strings' ? MutationInputValue : + TTable extends 'objects' ? MutationInputValue : + MutationInputLink +)[] + +export type Options = { + table?: TTable; + tableNamePostfix?: string; + returning?: string; + variables?: any; + name?: string; + aggregate?: 'count' | 'sum' | 'avg' | 'min' | 'max'; +}; + +export type ReadOptions = Options; + +export type WriteOptions = Options & { + silent?: boolean; +} + diff --git a/imports/minilinks-query.ts b/imports/minilinks-query.ts index f881a1bb..def9bca8 100644 --- a/imports/minilinks-query.ts +++ b/imports/minilinks-query.ts @@ -1,16 +1,16 @@ import { _serialize, _boolExpFields, serializeWhere, serializeQuery } from './client.js'; import { BoolExpLink, ComparasionType, QueryLink } from './client_types.js'; -import { MinilinkCollection, MinilinksGeneratorOptions, Link } from './minilinks.js'; +import { MinilinkCollection, MinilinksGeneratorOptions, Link, Id } from './minilinks.js'; export interface BoolExpLinkMinilinks extends BoolExpLink { - _applies?: ComparasionType; + _applies?: ComparasionType; } -export const minilinksQuery = >( - query: QueryLink | number, +export const minilinksQuery = >( + query: QueryLink | Id, ml: MinilinkCollection, ): L[] => { - if (typeof(query) === 'number') return [ml.byId[query]]; + if (typeof(query) === 'number' || typeof(query) === 'string') return [ml.byId[query]]; else { const q = serializeQuery(query); const result = minilinksQueryHandle(q.where, ml); @@ -18,7 +18,7 @@ export const minilinksQuery = >( } }; -export const minilinksQueryIs = >( +export const minilinksQueryIs = >( query: QueryLink | number, link: L, ): boolean => { @@ -33,7 +33,7 @@ export const minilinksQueryIs = >( } }; -export const minilinksQueryHandle = >( +export const minilinksQueryHandle = >( q: BoolExpLinkMinilinks, ml: MinilinkCollection, ): L[] => { @@ -49,7 +49,7 @@ export const minilinksQueryHandle = >( export const minilinksQueryLevel = ( q: BoolExpLinkMinilinks, - link: Link, + link: Link, env: string = 'links', ) => { const fields = Object.keys(q); @@ -140,7 +140,7 @@ export const minilinksQueryLevel = ( export const minilinksQueryComparison = ( q: BoolExpLinkMinilinks, - link: Link, + link: Link, field: string, env: string = 'links', ): boolean => { diff --git a/imports/minilinks.ts b/imports/minilinks.ts index 6050a787..cf9e9f3c 100644 --- a/imports/minilinks.ts +++ b/imports/minilinks.ts @@ -19,7 +19,9 @@ const log = debug.extend('log'); const error = debug.extend('error'); // Force enable this file errors output -export interface LinkPlain { +export type Id = number | string; + +export interface LinkPlain { id: Ref; type_id: Ref; from_id?: Ref; @@ -27,13 +29,13 @@ export interface LinkPlain { value?: any; } -export interface LinkRelations> { +export interface LinkRelations> { typed: L[]; type: L; in: L[]; - inByType: { [id: number]: L[] }; + inByType: { [id: string]: L[] }; out: L[]; - outByType: { [id: number]: L[] }; + outByType: { [id: string]: L[] }; from: L; to: L; value?: any; @@ -42,28 +44,28 @@ export interface LinkRelations> { } export interface LinkHashFields { - [key: string|number]: any; + [key: Id]: any; } -export interface Link extends LinkPlain, LinkRelations>, LinkHashFields {} +export interface Link extends LinkPlain, LinkRelations>, LinkHashFields {} export type MinilinksQueryOptionAggregate = 'count' | 'sum' | 'avg' | 'min' | 'max'; export interface MinilinksQueryOptions { aggregate?: A; } -export interface MinilinksResult> { +export interface MinilinksResult> { links: L[]; - types: { [id: number]: L[] }; - byId: { [id: number]: L }; - byFrom: { [id: number]: L[] }; - byTo: { [id: number]: L[] }; - byType: { [id: number]: L[] }; + types: { [id: Id]: L[] }; + byId: { [id: Id]: L }; + byFrom: { [id: Id]: L[] }; + byTo: { [id: Id]: L[] }; + byType: { [id: Id]: L[] }; options: MinilinksGeneratorOptions; emitter: EventEmitter; - query(query: QueryLink | number): L[] | any; - select(query: QueryLink | number): L[] | any; - subscribe(query: QueryLink | number): Observable; + query(query: QueryLink | Id): L[] | any; + select(query: QueryLink | Id): L[] | any; + subscribe(query: QueryLink | Id): Observable; add(linksArray: any[]): { anomalies?: MinilinkError[]; errors?: MinilinkError[]; @@ -79,7 +81,7 @@ export interface MinilinksResult> { } } -export class MinilinksLink { +export class MinilinksLink { ml?: MinilinkCollection; id: Ref; type_id: Ref; @@ -185,12 +187,12 @@ export const MinilinksGeneratorOptionsDefault: MinilinksGeneratorOptions = { Link: MinilinksLink, }; -export interface MinilinksInstance>{ +export interface MinilinksInstance>{ (linksArray: L[], memory?: MinilinksResult): MinilinksResult } -export function Minilinks>(options: MGO): MinilinksInstance { - return function minilinks>(linksArray = [], memory: any = {}): MinilinksResult { +export function Minilinks>(options: MGO): MinilinksInstance { + return function minilinks>(linksArray = [], memory: any = {}): MinilinksResult { // @ts-ignore const mc = new MinilinkCollection(options, memory); mc.add(linksArray); @@ -200,23 +202,23 @@ export function Minilinks = Link> { +export class MinilinkCollection = Link> { useMinilinksQuery = useMinilinksQuery; useMinilinksFilter = useMinilinksFilter; useMinilinksApply = useMinilinksApply; useMinilinksSubscription = useMinilinksSubscription; useMinilinksHandle = useMinilinksHandle; - types: { [id: number]: L[] } = {}; - byId: { [id: number]: L } = {}; - byFrom: { [id: number]: L[] } = {}; - byTo: { [id: number]: L[] } = {}; - byType: { [id: number]: L[] } = {}; + types: { [id: Id]: L[] } = {}; + byId: { [id: Id]: L } = {}; + byFrom: { [id: Id]: L[] } = {}; + byTo: { [id: Id]: L[] } = {}; + byType: { [id: Id]: L[] } = {}; links: L[] = []; options: MGO; emitter: EventEmitter; - query(query: QueryLink | number, options?: MinilinksQueryOptions): A extends string ? any : L[] { + query(query: QueryLink | Id, options?: MinilinksQueryOptions): A extends string ? any : L[] { const result = minilinksQuery(query, this); if (options?.aggregate === 'count') return result?.length as any; if (options?.aggregate === 'avg') return _mean(result?.map(l => l?.value?.value)) as any; @@ -225,7 +227,7 @@ export class MinilinkCollection l?.value?.value)) as any; return result; } - select(query: QueryLink | number, options?: MinilinksQueryOptions): L[] | any { + select(query: QueryLink | Id, options?: MinilinksQueryOptions): L[] | any { return this.query(query, options); } @@ -234,7 +236,7 @@ export class MinilinkCollection {}, error: (err) => {} }); */ - subscribe(query: QueryLink | number): Observable { + subscribe(query: QueryLink | Id): Observable { const ml = this; return new Observable((observer) => { let prev = ml.query(query); @@ -394,24 +396,24 @@ export class MinilinkCollection r.id === id); + // _remove(link?.[options.to]?.[options.in], (r: { id?: Id }) => r.id === id); // link.out += byFrom[link.id] // XXX - // _remove(link?.[options.from]?.[options.out], (r: { id?: number }) => r.id === id); + // _remove(link?.[options.from]?.[options.out], (r: { id?: Id }) => r.id === id); // byFrom[link.from_id]: link[]; // XXX - _remove(byFrom?.[link?.[options.from_id]] || [], (r: { id?: number }) => r.id === id); + _remove(byFrom?.[link?.[options.from_id]] || [], (r: { id?: Id }) => r.id === id); // byTo[link.to_id]: link[]; // XXX - _remove(byTo?.[link?.[options.to_id]] || [], (r: { id?: number }) => r.id === id); + _remove(byTo?.[link?.[options.to_id]] || [], (r: { id?: Id }) => r.id === id); // byType[link.type_id]: link[]; // XXX - _remove(byType?.[link?.[options.type_id]] || [], (r: { id?: number }) => r.id === id); + _remove(byType?.[link?.[options.type_id]] || [], (r: { id?: Id }) => r.id === id); // from.outByType[link.type_id] += link; // XXX - // _remove(link?.[options.from]?.outByType?.[link.type_id] || [], (r: { id?: number }) => r.id === id) + // _remove(link?.[options.from]?.outByType?.[link.type_id] || [], (r: { id?: Id }) => r.id === id) // to.inByType[link.type_id] += link; // XXX - // _remove(link?.[options.to]?.inByType?.[link.type_id] || [], (r: { id?: number }) => r.id === id) + // _remove(link?.[options.to]?.inByType?.[link.type_id] || [], (r: { id?: Id }) => r.id === id) // for (let i = 0; i < byFrom?.[id]?.length; i++) { // const dep = byFrom?.[id]?.[i]; @@ -520,12 +522,12 @@ export class MinilinkCollection> { +export interface MinilinksHookInstance> { ml: MinilinksResult; ref: { current: MinilinksResult; }; } -export function useMinilinksConstruct>(options?: any): MinilinksHookInstance { +export function useMinilinksConstruct>(options?: any): MinilinksHookInstance { // @ts-ignore const mlRef = useRef>(useMemo(() => { // @ts-ignore @@ -535,7 +537,7 @@ export function useMinilinksConstruct>(options?: any): Mi return { ml, ref: mlRef }; } -export function useMinilinksFilter, R = any>( +export function useMinilinksFilter, R = any>( ml, filter: (currentLink: L, oldLink: L, newLink: L) => boolean, results: (l?: L, ml?: any, oldLink?: L, newLink?: L) => R, @@ -597,7 +599,7 @@ export function useMinilinksFilter, R = any>( return state; }; -export function useMinilinksHandle>(ml, handler: (event, oldLink, newLink) => any): void { +export function useMinilinksHandle>(ml, handler: (event, oldLink, newLink) => any): void { useEffect(() => { const addedListener = (ol, nl) => { handler('added', ol, nl); @@ -624,7 +626,7 @@ export function useMinilinksHandle>(ml, handler: (event, }, []); }; -export function useMinilinksApply>(ml, name: string, data?: L[]): any { +export function useMinilinksApply>(ml, name: string, data?: L[]): any { const [strictName] = useState(name); useEffect(() => { return () => { @@ -638,7 +640,7 @@ export function useMinilinksApply>(ml, name: string, data * React hook. Returns reactiviely links from minilinks, by query in deeplinks dialect. * Recalculates when query changes. (Take query into useMemo!). */ -export function useMinilinksQuery>(ml, query: QueryLink | number) { +export function useMinilinksQuery>(ml, query: QueryLink | Id) { return useMemo(() => ml.query(query), [ml, query]); }; @@ -646,7 +648,7 @@ export function useMinilinksQuery>(ml, query: QueryLink | * React hook. Returns reactiviely links from minilinks, by query in deeplinks dialect. * Recalculates when data in minilinks changes. (Take query into useMemo!). */ -export function useMinilinksSubscription>(ml, query: QueryLink | number) { +export function useMinilinksSubscription>(ml, query: QueryLink | Id) { const [d, setD] = useState(); const sRef = useRef(); const qPrevRef = useRef(query);