iyio-common
Common types, functions and classes used by the IYIO (eye·o) framework.
The core requirement for code to be placed in the iyio-common library is based on dependencies.
Only the following dependencies are allowed to be used in the iyio-common library:
Index
- Scopes - Dependency injection and application configuration
- HttpClient - An http for use in Node and the browser
- QueryObjects - An interface for representing custom client SQL select queries.
- Query Client - A client for retrieving data using Query Objects
- Short UUIDs - A utility function for generating short UUIDs
- uiRouterService - Application and framework agnostic navigation
- ObjWatcher - A framework agnostic signals like implementation
Scopes
Scopes provided a form of dependency injection that is used by many of the classes and service
with in iyio. Scopes can be used to define services, clients, providers and configuration values.
By default scoped objects use a shared rootScope which makes using scoped objects transparent
in most cases
Defining scoped objects
A scoped object is a function that returns a value defined within a scope. The scope the value is
created in also contains all of its dependencies.
When defining a scoped object use can supply a default value for the object or allow a value to be
provided later during scope initialization.
Use the functions below to define scoped objects.
declare function defineProvider<T>(
name:string,
defaultProvider?:TypeProvider<T>|TypeProviderOptions<T>
):ProviderTypeDef<T>;
declare function defineFactory<T extends AnyFunction>(
name:string,
defaultFactory?:T
):FactoryTypeDef<T>;
declare function defineAsyncFactory<T extends AnyAsyncFunction>(
name:string,
defaultFactory?:T
):FactoryTypeDef<T>;
declare function defineService<T>(
name:string,
defaultProvider?:TypeProvider<T>|TypeProviderOptions<T>
):ServiceTypeDef<T>;
declare function defineClient<T>(
name:string,
defaultProvider?:TypeProvider<T>|TypeProviderOptions<T>
):ClientTypeDef<T>;
declare function defineServiceFactory<T>(
name:string,
provider:TypeProvider<T>
):ServiceTypeDef<T>;
declare function defineClientFactory<T>(
name:string,
provider:TypeProvider<T>
):ClientTypeDef<T>;
declare function defineParam<T>(
name:string,
valueConverter?:(str:string,scope:Scope)=>T,
defaultValue?:T
):ParamTypeDef<T>;
declare function defineStringParam(name:string,defaultValue?:string):ParamTypeDef<string>;
declare function defineNumberParam(name:string,defaultValue?:number):ParamTypeDef<number>;
declare function defineBoolParam(name:string,defaultValue?:boolean):ParamTypeDef<boolean>;
Scope initialization
During scope initialization you can provide implementations and param values for scoped objects.
Values provided during initialization will override default values.
import {
authModule, httpParamsModule, isServerSide,
ScopeModulePriorities, ScopeRegistration, UserFactory
} from "@iyio/common";
import { UserCtrl } from '../lib/UserCtrl';
import { FunnyMan } from '../lib/FunnyMan';
import { jokeClient } from '../lib/scoped-types';
export const appModule=(reg:ScopeRegistration)=>{
reg.addParams({
"apiUrl":"api.example.com",
"primaryColor":"#00ff00",
"funnyJoke":"What do you call an alligator detective? An investi-gator 😂",
})
reg.implementClient(jokeClient,(scope)=>new FunnyMan(scope))
reg.addFactory(UserFactory,options=>new UserCtrl(options));
reg.use(authModule);
reg.use(httpParamsModule);
reg.use({
priority:ScopeModulePriorities.postConfig,
init:scope=>{
}
})
}
initRootScope(appModule);
const newScope=createScope(appModule);
Async initialization
If a scope has any modules that use async initializers then scope initialization will be asynchronous
and should be awaited. You can use the getInitPromise
function of a scope to get a promise
that will complete once scope initialization is finished.
await rootScope.getInitPromise();
const newScope=createScope(appModule);
await newScope.getInitPromise();
Using scoped objects
After a scoped object is defined it can be used by calling the function returned by one of the
define scoped object functions.
interface IJokeClient
{
getJoke():string;
}
const jokeClient=defineClient<IJokeClient>('jokeClient');
const joke=jokeClient().getJoke();
HttpClient
The HttpClient class provided a set of methods for all the common HTTP methods and automatically
handles retries, deserialization, authentication and other common HTTP requirements.
IYIO defines a scoped client named httpClient
that can be used to access the default HttpClient.
Usage
import { httpClient } from "@iyio/common";
interface Joke
{
id:number;
joke:string;
score:number;
}
const jokeId=await httpClient().postAsync<number,Omit<Joke,'id'>>('https://api.exaple.com/jokes',{
joke:'What do you call an alligator detective? An investi-gator 😂',
score:1
})
const joke=await httpClient().getAsync<Joke>(`https://api.exaple.com/jokes/${jokeId}`);
if(!joke){
console.log('404 joke not found');
return;
}
console.log(`joke: ${joke.joke}\nscore: ${joke.score}`);
main();
HttpClient methods
class HttpClient{
public async getAsync<TReturn>(uri:string,options?:HttpClientRequestOptions):Promise<TReturn|undefined>;
public async getStringAsync(uri:string,options?:HttpClientRequestOptions):Promise<string|undefined>;
public getResponseAsync(uri:string,options?:HttpClientRequestOptions):Promise<Response|undefined>;
public async postAsync<TReturn,TBody=any>(uri:string,body:TBody,options?:HttpClientRequestOptions):Promise<TReturn|undefined>;
public async patchAsync<TReturn,TBody=any>(uri:string,body:TBody,options?:HttpClientRequestOptions):Promise<TReturn|undefined>;
public async putAsync<TReturn,TBody=any>(uri:string,body:TBody,options?:HttpClientRequestOptions):Promise<TReturn|undefined>;
public async deleteAsync<TReturn>(uri:string,options?:HttpClientRequestOptions):Promise<TReturn|undefined>;
public applyBaseUrl(uri:string):string;
public async requestAsync<T>(
method:HttpMethod,
uri:string,
body?:any,
options?:HttpClientRequestOptions
):Promise<T|undefined>;
}
export interface HttpClientOptions
{
baseUrlMap?:HashMap<string>;
signers?:TypeDef<HttpRequestSigner>;
fetchers?:TypeDef<HttpFetcher>;
baseUrlPrefix?:string;
jwtProviders?:TypeDef<JwtProvider>;
logRequests?:boolean;
logResponses?:boolean;
maxRetries?:number;
retryDelay?:number;
}
Query Objects
Query Objects provide a consistent interface for representing custom SQL queries that are constructed
from user input.
For example imagine a component with a dropdown menu that allows users to view different customer
profiles based on events generated by the customers. As different events are selected in the dropdown
a query object is updated to reflect the changes without the need to allow for arbitrary SQL queries
created in the frontend or the need for a dedicated api endpoint.
<select id="event-type">
<option value="pageView">Page View</option>
<option value="cartAdd" selected>Add to Cart</option>
<option value="cartAbandon">Abandon Cart</option>
</select>
const eventType=document.querySelector('#event-type').value;
const customerQuery:Query={
table:"Profile",
columns:[
{col:"id",name:"id"},
{col:"name",name:"name"},
{col:"email",name:"email"},
],
condition:{
left:{col:"id"},
op:'in',
right:{subQuery:{query:{
table:"EventRecord",
columns:[{col:"profileId",name:"profileId"}],
condition:{
op:'and',
conditions:[
{
left:{col:"profileId"},
op:'=',
right:{col:{name:"id",target:"parent"}}
},
{
left:{col:"type"},
op:'=',
right:{value:eventType}
}
]
},
limit:1
}}}
},
limit: 20,
}
The customerQuery Query Object can now be sent to an API endpoint to execute the query and the query
is guaranteed to be save to execute and free of and SQL injections.
The buildQuery
function can be used to convert the Query Object into SQL. Below is the result of
passing the above customerQuery to the buildQuery
function.
select "id" as "id" , "name" as "name" , "email" as "email"
from "Profile" as "_tbl_1"
where (
( "id" ) in ( (
select "profileId" as "profileId"
from "EventRecord" as "_tbl_2"
where ( ( ( "profileId" ) = ( "_tbl_1"."id" ) ) and ( ( "type" ) = ( 'cartAdd' ) ) )
limit 1
) )
) limit 20
The SQL generated by buildQuery
is a little on the verbose side but insures proper order of
operations in all situations.
Query Object interfaces
interface Query
{
id?:string;
columns?:NamedQueryValue[];
table?:string|Query;
tableAs?:string;
join?:QueryJoin|QueryJoin[];
condition?:QueryConditionOrGroup;
match?:HashMap;
orderBy?:OrderCol|OrderCol[];
groupBy?:string|string[];
limit?:number;
offset?:number;
disableUserOrderBy?:boolean;
isUserReadonly?:boolean;
debug?:boolean;
label?:string;
passthrough?:Query;
metadata?:Record<string,any>;
}
interface SubQuery
{
condition?:QueryConditionOrGroup;
query:Query;
}
interface QueryJoin
{
table:string;
tableAs?:string;
condition:QueryConditionOrGroup;
required?:boolean;
}
interface QueryWithData<T=any> extends Omit<Query,'table'>
{
table:T[];
}
type QueryOrQueryWithData<T=any>=Query|QueryWithData<T>;
type QueryGroupConditionOp='and'|'or';
interface QueryGroupCondition
{
op:QueryGroupConditionOp;
conditions:QueryConditionOrGroup[];
label?:string;
metadata?:Record<string,any>;
}
type QueryConditionOp='='|'!='|'>'|'<'|'>='|'<='|'like'|'is'|'in'
interface QueryCondition
{
left:QueryValue;
op:QueryConditionOp;
not?:boolean;
right:QueryValue;
label?:string;
metadata?:Record<string,any>;
}
type QueryConditionOrGroup=QueryCondition|QueryGroupCondition;
type QueryFunction=(
'count'|'sum'|'avg'|'min'|'max'|'round'|
'lower'|'upper'|'len'|'trim'|'ltrim'|'rtrim'|'concat'|'replace'|'strcmp'|
'reverse'|'coalesce'
)
type QueryExpressionOperator=(
'('|')'|'-'|'+'|'/'|'*'|'%'|'&'|'|'|'^'|'='|'>'|'<'|'>='|'>='|'<>'|
'all'|'and'|'any'|'between'|'exists'|'in'|'like'|'not'|'or'|'some'
)
type QueryGeneratedValue={
type:'timeMs';
offset?:number;
}|{
type:'timeSec';
offset?:number;
}
interface QueryValue
{
subQuery?:SubQuery;
col?:QueryCol|string;
value?:string|number|boolean|null|((string|number|boolean|null)[]);
generatedValue?:QueryGeneratedValue;
func?:QueryFunction|QueryFunction[];
args?:QueryValue[];
coalesce?:string|number|boolean|((string|number|boolean)[]);
expression?:(OptionalNamedQueryValue|QueryExpressionOperator)[];
}
interface OptionalNamedQueryValue extends QueryValue
{
name?:string;
}
interface NamedQueryValue extends QueryValue
{
name:string;
}
type QueryTargetTable=number|'none'|'current'|'parent';
interface QueryCol
{
name:string;
table?:string;
target?:QueryTargetTable;
}
interface OrderCol extends QueryCol
{
desc?:boolean;
}
Query Client
The queryClient()
scoped object provides a uniform interface for fetching data based on Query
Objects and is primarily intended to be used on the frontend as a secure way of allowing user input
to form SQL queries to select data.
import { Query, queryClient } from "@iyio/common";
interface MediaItem
{
id:number;
name:string;
contentType:string;
url:string
size:number;
}
const mediaQuery:Query={
table:"MediaItems",
condition:{
left:{col:"contentType"},
op:"like",
right:{value:"image/%"}
},
limit:20,
orderBy:{name:"size",desc:true}
}
const mediaItems=await queryClient().selectQueryItemsAsync(mediaQuery);
There is not default implementation for queryClient()
, one must be registered during scope
initialization. Is most cases a queryClient implementation will forward Query Objects to an api
endpoint for execution. queryClient implementations must implement the IQueryClient interface
interface IQueryClient
{
selectQueryItemsAsync<T=any>(query:Query):Promise<T[]>;
}
Short UUIDs
You can use the shortUuid()
function to generate URL friendly base64 encoded UUIDs. The base64
encoded UUIDs have the same uniqueness but are shorted since they are base64 encoded instead of
HEX encoded. The =
padding characters are removed and the +
and /
characters are replaced
with -
and _
so that the generated UUIDs are still valid URI components and file names.
Below are the characters used when generating short UUIDs
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_
import { shortUuid } from "@iyio/common";
console.log(shortUuid())
uiRouterService
The uiRouterService()
scoped service is an application and framework agnostic service that allows
a uniform interface for handing navigation in applications. It is roughly models around the browser
history api. The default implementation of uiRouterService()
uses the browser history api to
implement navigation. IYIO also offers an NextJS uiRouterService implementation called NextJsUiRouter
.
import { uiRouterService } from "@iyio/common";
uiRouterService().push('/example-page');
uiRouterService().pop();
Use with NextJS
To use uiRouterService()
with NextJS register the nextJsModule
during scope initialization.
import { nextJsModule, NextJsUiRouter } from "@iyio/nextjs-common";
initRootScope(scope=>{
scope.use(nextJsModule);
scope.implementService(uiRouterService,()=>new NextJsUiRouter());
})
ObjWatcher
ObjWatcher is a set of functions and classes that implement a signals like api that allow for
mutations of plain javascript objects to be observed and reacted to. This allows you to define
simple state objects for a complex components and allow child components can observe and make
changes to state.
import { wAryPush, wSetProp, watchObj } from "@iyio/common";
const person={
name:'Jeff',
age:45,
jobs:[
{position:'Cook',annualIncome:60000},
{position:'Janitor',annualIncome:55000}
],
hobby:{
name:'RC Car Racing',
hoursPerWeek:8,
}
}
const watcher=watchObj(person);
watcher.watchPath('name',()=>{
console.log(`Name set to ${person.name}`);
})
watcher.watchPath('hobby.name',()=>{
console.log(`Hobby set to ${person.hobby.name}`)
})
watcher.watchDeepPath('jobs',()=>{
console.log(`Jobs updated. count - ${person.jobs.length}`)
})
wSetProp(person,'name','Mark');
wSetProp(person.hobby,'name','Riding horses');
wSetProp(person,'hobby',{name:'Watching birds',hoursPerWeek:5});
wSetProp(person.jobs[0],'annualIncome',64000);
wAryPush(person.jobs,{position:'Car Sales',annualIncome:70000});
stopWatchingObj(person);
Watch Functions
Use the following functions to watch for changes to objects.
const watchObj=<T extends Watchable>(obj:T):ObjWatcher<T>;
const stopWatchingObj=<T>(obj:T):ObjWatcher<T>|undefined;
const getObjWatcher=<T>(obj:T,autoCreate:boolean):ObjWatcher<T>|undefined;
const watchObjDeep=<T extends Watchable>(
obj:T,
listener:ObjRecursiveListenerOptionalEvt,
options?:PathWatchOptions
):WatchedPath;
const watchObjAtPath=<T extends Watchable>(
obj:T,
path:RecursiveKeyOf<T>,
listener:ObjRecursiveListenerOptionalEvt,
options?:PathWatchOptions
):WatchedPath;
const watchObjWithFilter=<T extends Watchable>(
obj:T,
filter:ObjWatchFilter<T>,
listener:ObjRecursiveListenerOptionalEvt,
options?:PathWatchOptions
):WatchedPath;
Mutate Functions
Use the following functions to mutate objects and allow the mutations to be watched.
const wSetProp=<T,P extends keyof T>(obj:T|null|undefined,prop:P,value:T[P],source?:any):T[P];
const wSetOrMergeProp=<T,P extends keyof T>(obj:T|null|undefined,prop:P,value:T[P],source?:any):T[P];
const wToggleProp=<T,P extends keyof T>(obj:T|null|undefined,prop:P,source?:any):boolean;
const wSetPropOrDeleteFalsy=<T,P extends keyof T>(obj:T|null|undefined,prop:P,value:T[P]):T[P];
const wSetPropOrDeleteWhen=<T,P extends keyof T>(obj:T|null|undefined,prop:P,value:T[P],deleteWhen:T[P]):T[P];
const wDeleteProp=<T,P extends keyof T>(obj:T|null|undefined,prop:P,source?:any):void;
const wDeleteAllObjProps=(obj:any);
const wAryPush=<T extends Array<any>>(obj:T|null|undefined,...values:T[number][]);
const wArySplice=<T extends Array<any>>(
obj:T|null|undefined,index:number,deleteCount:number,...values:T[number][]
):boolean;
const wArySpliceWithSource=<T extends Array<any>>(
source:any,obj:T|null|undefined,index:number,deleteCount:number,values:T[number][]
):boolean;
const wAryRemove=<T extends Array<any>>(obj:T|null|undefined,value:any):boolean;
const wAryRemoveAt=<T extends Array<any>>(obj:T|null|undefined,index:number,count=1):boolean;
const wAryMove=<T extends Array<any>>(obj:T|null|undefined,fromIndex:number,toIndex:number,count=1,source?:any):boolean;
const wTriggerEvent=<T>(obj:T|null|undefined,type:string|symbol,value?:any,source?:any):void;
const wTriggerChange=<T>(obj:T|null|undefined,source?:any):void;
const wTriggerLoad=<T>(obj:T|null|undefined,prop?:keyof T,source?:any):void;