Last active
January 20, 2021 08:21
-
-
Save Kittoes0124/a8050730f4c700f8c6a69517519e5b2d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Known Issues: | |
0) Joining to the same table results in circular references. | |
1) Joining to any table results in the loss of all type information. | |
*/ | |
interface Array<T> { | |
toBtSqlTable(keySelector: IndexKeySelector<T>): TableWithOperators<T>; | |
} | |
type Action<T> = UnaryProjection<T, void>; | |
type BinaryPredicate<TFirst, TSecond> = BinaryProjection<TFirst, TSecond, boolean>; | |
type BinaryProjection<TFirst, TSecond, TResult> = (first: Readonly<TFirst>, second: Readonly<TSecond>) => TResult; | |
type IndexKey = (number | string); | |
type IndexKeySelector<T> = UnaryProjection<T, IndexKey>; | |
type TableWithOperators<TEntity> = { | |
forEach(action: Action<TEntity>): void; | |
join<TOther>( | |
other: TableWithOperators<TOther>, | |
...predicates: ReadonlyArray<BinaryPredicate<TEntity, TOther>> | |
): TableWithOperators<any>; | |
leftJoin<TOther>( | |
other: TableWithOperators<TOther>, | |
...predicates: ReadonlyArray<BinaryPredicate<TEntity, TOther>> | |
): TableWithOperators<any>; | |
select<TProjection>( | |
a: UnaryProjection<TEntity, TProjection>, | |
): TableWithOperators<TProjection>; | |
select<A, TProjection>( | |
a: UnaryProjection<TEntity, A>, | |
b: UnaryProjection<A, TProjection>, | |
): TableWithOperators<TProjection>; | |
select<A, B, TProjection>( | |
a: UnaryProjection<TEntity, A>, | |
b: UnaryProjection<A, B>, | |
c: UnaryProjection<B, TProjection>, | |
): TableWithOperators<TProjection>; | |
select<A, B, C, TProjection>( | |
a: UnaryProjection<TEntity, A>, | |
b: UnaryProjection<A, B>, | |
c: UnaryProjection<B, C>, | |
d: UnaryProjection<C, TProjection>, | |
): TableWithOperators<TProjection>; | |
select<A, B, C, D, TProjection>( | |
a: UnaryProjection<TEntity, A>, | |
b: UnaryProjection<A, B>, | |
c: UnaryProjection<B, C>, | |
d: UnaryProjection<C, D>, | |
e: UnaryProjection<D, TProjection>, | |
): TableWithOperators<TProjection>; | |
select(...projections: ReadonlyArray<UnaryProjection<any, any>>): TableWithOperators<any>; | |
toArray(): Array<TEntity>; | |
where(...predicates: ReadonlyArray<UnaryPredicate<TEntity>>): TableWithOperators<TEntity>; | |
}; | |
type UnaryPredicate<T> = UnaryProjection<T, boolean>; | |
type UnaryProjection<TInput, TResult> = (input: Readonly<TInput>) => TResult; | |
module BtSql { | |
Object.defineProperty(Array.prototype, "toBtSqlTable", { | |
value: function<T>(keySelector: IndexKeySelector<any>) { | |
return buildTableCore<T>(this, keySelector); | |
}, | |
}); | |
type CircularReferenceHandler = ( | |
parentKey: Readonly<IndexKey>, | |
parentValue: Dictionary<IndexKey, any>, | |
childKey: Readonly<IndexKey>, | |
childValue: ImmutableDictionary<IndexKey, any>, | |
) => void; | |
type Dictionary<TKey extends IndexKey, TValue> = Record<TKey, TValue>; | |
type Immutable<T> = { readonly [P in keyof T]: Readonly<T[P]>; }; | |
type ImmutableDictionary<TKey extends IndexKey, TValue> = Immutable<Dictionary<TKey, TValue>>; | |
type ImmutableTable<TEntity, TKey extends IndexKey> = Immutable<Table<TKey, TEntity>>; | |
type Table<TKey extends IndexKey, TEntity> = ({ | |
index: Array<Readonly<TKey>>; | |
values: Dictionary<TKey, TEntity>; | |
j: Array<TableRelation<any, any>>, | |
s: Array<UnaryProjection<any, any>>, | |
w: Array<UnaryPredicate<TEntity>>, | |
}); | |
type TableRelation<T, U> = { | |
other: TableWithOperators<U>, | |
isOuterJoin?: boolean, | |
predicates?: ReadonlyArray<BinaryPredicate<T, U>>, | |
}; | |
const ERROR_TAG_DUPLICATE_KEY = "[Duplicate Key]"; | |
const ERROR_TAG_NOT_SUPPORTED = "[Not Supported]"; | |
const ERROR_MESSAGE_TABLE_BUILD_DUPLICATE_KEY = `${ERROR_TAG_DUPLICATE_KEY} Unable to build table; the index key must be unique within the data set.`; | |
const ERROR_MESSAGE_TABLE_JOIN_PROJECTION_NOT_SUPPORTED = `${ERROR_TAG_NOT_SUPPORTED} Unable to process join predicates; the feature is under review.`; | |
const assignObject = Object.assign; | |
const brandAndFreezeTable = <TEntity,>( | |
table: Table<IndexKey, TEntity>, | |
): TableWithOperators<TEntity> => { | |
freezeObject(table.index); | |
freezeObject(table.values); | |
brandedTables.add(table); | |
return freezeObject(table as any); | |
}; | |
const buildTableCore = <TEntity,>( | |
entities: ReadonlyArray<TEntity>, | |
keySelector: IndexKeySelector<TEntity>, | |
): TableWithOperators<TEntity> => brandAndFreezeTable(entities.reduce( | |
(t, e) => { | |
const k = keySelector(e); | |
if (t.values[k]) { | |
throw newError(ERROR_MESSAGE_TABLE_BUILD_DUPLICATE_KEY); | |
} | |
if (isObject(e)) { | |
processCircularReferences((e as any), defaultCircularReferenceHandler); | |
} | |
freezeEntityAndInsertIntoTable(cloneObjectShallow(e), k, t); | |
return t; | |
}, | |
newEmptyTableObject<TEntity>() | |
)); | |
const cloneObjectShallow = <T,>( | |
t: T, | |
): T => assignObject({}, t); | |
const defaultCircularReferenceHandler = ( | |
_parentKey: Readonly<IndexKey>, | |
parentValue: Dictionary<IndexKey, any>, | |
childKey: Readonly<IndexKey>, | |
_childValue: ImmutableDictionary<IndexKey, any>, | |
): void => { | |
parentValue[childKey] = undefined; | |
}; | |
const freezeEntityAndInsertIntoTable = <TEntity,>( | |
entity: TEntity, | |
key: IndexKey, | |
target: Table<IndexKey, TEntity>, | |
): void => { | |
target.index.push(key); | |
target.values[key] = freezeObject(entity); | |
}; | |
const freezeObject = Object.freeze; | |
const getAliasedTableValue = ( | |
charCode: number, | |
value: any, | |
): ImmutableDictionary<string, any> => ({ | |
[String.fromCharCode(charCode)]: (isObject(value) ? cloneObjectShallow(value) : { value, }), | |
}); | |
const getObjectProperties = Object.entries; | |
const identity = <T,>(input: T) => input; | |
const isObject = <T,>(input: T) => (input && ("object" === typeof input)); | |
const newArray = <T,>() => new Array<T>(); | |
const newError = (message?: string) => new Error(message); | |
const newEmptyTableObject = <TEntity,>( | |
): Table<IndexKey, TEntity> => { | |
let index = newArray<IndexKey>(); | |
let values = ({} as Dictionary<IndexKey, TEntity>); | |
let table = ({ | |
j: newArray<TableRelation<any, any>>(), | |
s: newArray<UnaryProjection<any, any>>(), | |
w: newArray<UnaryPredicate<any>>(), | |
index, | |
values, | |
forEach: function(action: Action<any>) { | |
for (const key of index) { | |
const value = projectMany(this.s)(values[key]); | |
if (projectMany(this.w)(value)) { | |
if (!this.j.length) { | |
action(value); | |
} | |
else { | |
let aliasCode = 97; | |
let isMatch = false; | |
let result = getAliasedTableValue(aliasCode, value); | |
for (const relation of this.j) { | |
++aliasCode; | |
const joinPredicates = relation.predicates; | |
const otherTable = ((relation.other as any) as Table<IndexKey, any>); | |
if (!joinPredicates?.length) { | |
let otherValue = otherTable.values[key]; | |
if ((otherValue && projectMany(otherTable.w)(otherValue)) || relation.isOuterJoin) { | |
isMatch = true; | |
otherValue = {}; | |
} | |
assignObject(result, getAliasedTableValue(aliasCode, otherValue)); | |
} | |
else { | |
throw newError(ERROR_MESSAGE_TABLE_JOIN_PROJECTION_NOT_SUPPORTED); | |
} | |
} | |
if (isMatch) { | |
action(result); | |
} | |
} | |
} | |
} | |
}, | |
join: function(other: TableWithOperators<any>, ..._predicates: ReadonlyArray<BinaryPredicate<any, any>>) { | |
this.j.push({ other: other, }); | |
return this; | |
}, | |
leftJoin: function(other: TableWithOperators<any>, ..._predicates: ReadonlyArray<BinaryPredicate<any, any>>) { | |
this.j.push({ | |
isOuterJoin: true, | |
other: other, | |
}); | |
return this; | |
}, | |
select: function(...p: ReadonlyArray<UnaryProjection<any, any>>) { | |
return brandAndFreezeTable(assignObject({ s: this.s.concat(p), }, this)); | |
}, | |
toArray: function(){ | |
const result = newArray<any>(); | |
this.forEach(value => result.push(value)); | |
return result; | |
}, | |
where: function(...p: ReadonlyArray<UnaryPredicate<any>>) { | |
return brandAndFreezeTable(assignObject({ w: this.w.concat(p), }, this)); | |
}, | |
} as (Table<IndexKey, TEntity> & TableWithOperators<TEntity>)); | |
return table; | |
}; | |
const newWeakSet = <T extends object>() => new WeakSet<T>(); | |
const processCircularReferences = <T extends object>( | |
input: T, | |
handler: CircularReferenceHandler, | |
initialPathValue = "o", | |
): T => { | |
const objectStack = [{ k: initialPathValue, v: (input as ImmutableDictionary<IndexKey, any>), },]; | |
const objectTracker = newWeakSet<object>(); | |
objectTracker.add(input); | |
while (objectStack.length) { | |
const { k: parentKey, v: parentValue } = objectStack.shift()!; | |
for (const [childKey, childValue] of getObjectProperties(parentValue)) { | |
if (isObject(childValue)) { | |
if (objectTracker.has(childValue)) { | |
handler(parentKey, parentValue, childKey, childValue); | |
} | |
else { | |
objectStack.push({ k: `${parentKey}.${childKey}`, v: childValue, }); | |
objectTracker.add(childValue); | |
} | |
} | |
} | |
} | |
return input; | |
} | |
const projectMany = ( | |
projections: Array<UnaryProjection<any, any>>, | |
): UnaryProjection<any, any> => { | |
if (0 === projections.length) { | |
return (identity as UnaryProjection<any, any>); | |
} | |
if (1 === projections.length) { | |
return projections[0]; | |
} | |
return (input: any): any => { | |
let result = input; | |
for (const projection of projections) { | |
const temp = projection(result); | |
if (terminationToken === temp) { | |
break; | |
} | |
result = temp; | |
} | |
return result; | |
} | |
}; | |
const brandedTables = newWeakSet<ImmutableTable<any, IndexKey>>(); | |
const terminationToken = {}; | |
export function act<T>(action: UnaryProjection<T, void>) { | |
return (input: T) => { | |
action(input); | |
return input; | |
}; | |
} | |
export function terminate<T>() { | |
return (((_input: T) => { | |
return terminationToken; | |
}) as UnaryProjection<T, any>); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment