-
-
Save calebmer/c26444edd35e29ff3b612966c3f982f3 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
// TODO: Validate catalog function. There are a lot of assumptions we make that | |
// cannot be statically typed. We should have a test utility function to prove | |
// those assumptions are correct. We will mostly test things not defined in | |
// the type system. | |
/** | |
* In order to build awesome tools from any database we need an abstract | |
* static data definition language. Catalog is the root level interface | |
* to this definition. | |
* | |
* If a database is strongly typed (like PostgreSQL) it may implement the | |
* catalog interfaces to expose its underlying data. | |
* | |
* Note that all methods on all catalog objects are assumed to be pure. This | |
* means any function called with the same input will yield the same result. | |
*/ | |
interface Catalog { | |
/** | |
* Get all of the types in our system. For any value that can be retrieved by | |
* the catalog there is an associated type. This strongly typed nature allows | |
* us to do many interesting things. Some sample types that may be returned | |
* by this method are enums or objects. We cannot directly access the | |
* underlying data a type represents through a type. For that we must go | |
* through a collection. | |
* | |
* Two types in this set should not have the same name. | |
*/ | |
// TODO: Test that types do not have the same name. | |
getTypes (): Iterator<Type<any>> | |
/** | |
* Gets all of the collections in our system. A collection will always have | |
* an object type, which should be accessible through `getTypes`. | |
* | |
* Two collections in this set should not have the same name. | |
*/ | |
// TODO: Test that the collection object type is returned by `getTypes`. | |
// TODO: Test that collections do not have the same name. | |
getCollections (): Iterator<Collection<any, any>> | |
} | |
/** | |
* Any type in our system. | |
* | |
* Every type object has (in it’s type definition) an associated type, named | |
* `TValue`. The difference between a `Type` and `TValue` (often expressed | |
* together as `Type<TValue>`) is important to understand. A `Type` we can | |
* statically introspect, getting the name, description, and more information. | |
* `TValue` represents the compiler type of the `Type` object’s internal value. | |
* So both `Type` and `TValue` represent the same thing, the difference is in | |
* the usage. `TValue` is statically defined in code used by the compiler | |
* whereas `Type` is defined at runtime. | |
*/ | |
interface Type<TValue> { | |
/** | |
* Gets the name for our type. Every type must have a name. | |
*/ | |
getName (): string | |
/** | |
* Gets an optional description for our type. | |
*/ | |
getDescription (): string | undefined | |
/** | |
* Determines if the provided value is valid for this type. For example, a | |
* string type with a `TValue` of `string` may implement this method as | |
* `typeof value === 'string'`. | |
*/ | |
isTypeOf (value: any): value is TValue | |
} | |
/** | |
* An object type is made up of many different fields, and a field is composed of a | |
* name and a type. This makes an object type a composite type as it is | |
* composed of many different types. | |
*/ | |
// TODO: Make sure no two fields have the same name. | |
export interface ObjectType<TValue> extends Type<TValue> { | |
/** | |
* Gets all of the fields on our object type. | |
*/ | |
getFields (): ObjectField<TValue, any>[] | |
} | |
/** | |
* An object field represents a single field on an object type. | |
*/ | |
export interface ObjectField<TObjectValue, TValue> { | |
/** | |
* Gets the name of our field. | |
*/ | |
getName (): string | |
/** | |
* Gets the optional description for our field. | |
*/ | |
getDescription (): string | undefined | |
/** | |
* Gets the type of our field. | |
*/ | |
getType (): Type<TValue> | |
/** | |
* This method takes the object value for this field, and extracts the field | |
* value from it. | |
*/ | |
getFieldValueFromObjectValue (object: TObjectValue): TValue | |
} | |
/** | |
* A collection represents a set of typed values that can be operated on in | |
* basic CRUD fashion in our system. | |
*/ | |
export interface Collection<TValue> { | |
/** | |
* Gets the name of our collection. | |
*/ | |
getName (): string | |
/** | |
* Gets the optional description of our collection. | |
*/ | |
getDescription (): string | undefined | |
/** | |
* Get the type of *all* the values in our collection. | |
*/ | |
getType (): ObjectType<TValue> | |
/** | |
* Get all of the unique identifiers for this collection. A key is a token | |
* that can be used to select and reselect any singular value in a collection | |
* by. | |
*/ | |
// TODO: Test that we don’t have any keys with the same name. | |
getKeys (): CollectionKey<TValue, any>[] | |
/** | |
* Gets the primary unique identifier for this collection. While a | |
* collection may have many keys, only one is the *primary* identifier. | |
* However, a collection may not have a primary key. | |
*/ | |
getPrimaryKey (): CollectionKey<TValue, any> | undefined | |
/** | |
* Get all the relations for which this collection is the tail. | |
* That means the relations in which the values of this collection point to | |
* the values of other collections. | |
* | |
* @see CollectionRelation | |
*/ | |
// TODO: Make sure we don’t have any tail relations with the same name. | |
// TODO: Make sure the relation reference to this collection is valid. | |
getTailRelations (): CollectionRelation<TValue, any, any>[] | |
/** | |
* Get all the relations for which this collection is the head. That means | |
* the relations in which another collections values point to a value in this | |
* collection. | |
* | |
* @see CollectionRelation | |
*/ | |
// TODO: Make sure we don’t have any head relations with the same name. | |
// TODO: Make sure the relation reference to this collection is valid. | |
getHeadRelations (): CollectionRelation<any, TValue, any>[] | |
/** | |
* Creates a value in our collection. Returns the newly created value. | |
* Helpful if the value passed in was missing any default values. | |
*/ | |
// TODO: Test that we can use this method on an empty collection and then | |
// use all the other methods to interact with our created objects. | |
create (value: TValue): Promise<TValue> | |
/** | |
* Reads a subset of the collection’s values. This is returned as an | |
* observable to allow for streaming the values one at a time to the | |
* consumer. | |
*/ | |
// TODO: Filter object. | |
// TODO: Cursors. | |
// TODO: Test that the config is correctly applied. The condition should | |
// have passed, the limit should be correct, and the skip (hard to test) | |
// should pass. | |
readMany (config: CollectionValueSetConfig): Observable<TValue> | |
} | |
/** | |
* The object that is used to configure the subset of values we want when | |
* reading from the collection. | |
*/ | |
type CollectionValueSetConfig = { | |
/** | |
* The filter with which we will narrow down our result set. To use many | |
* conditions, look at `AndCondition` and `OrCondition`. | |
*/ | |
filter?: Condition, | |
/** | |
* The maximum number of values to be returned. | |
*/ | |
limit?: number, | |
/** | |
* How many values to skip over before we start returning the values. | |
*/ | |
skip?: number, | |
} | |
/** | |
* The condition type defines a set of constraints that we may apply to any | |
* value that may be represented in our system. A condition must be defined | |
* declaratively so that any query language may interpret the condition. | |
* | |
* Note that a condition is loosely typed. There is no way to statically | |
* determine if a condition is appropriate for any type in our system. At | |
* runtime we should determine if a condition is appropriate for a given type. | |
* *Condition’s that are not appropriate should be rejected by collection | |
* implementations*. | |
*/ | |
// TODO: Create helper functions for building conditions. | |
// TODO: Test arbitrary conditions with arbitrary types to ensure that the | |
// condition would pass for a given type. | |
// TODO: Consider a text search condition. | |
// TODO: Consider some geographic operators. | |
// TODO: Consider some array operators. | |
type Condition = | |
TrueCondition | | |
FalseCondition | | |
NotCondition | | |
AndCondition | | |
OrCondition | | |
FieldCondition | | |
EqualCondition | | |
LessThanCondition | | |
GreaterThanCondition | | |
RegexpCondition | |
/** | |
* This condition always passes. | |
*/ | |
type TrueCondition = { | |
type: 'TRUE', | |
} | |
/** | |
* This condition always fails. | |
*/ | |
type FalseCondition = { | |
type: 'FALSE', | |
} | |
/** | |
* Inverts the result of a condition. If a condition would be true, it is now | |
* false. | |
*/ | |
type NotCondition = { | |
type: 'NOT', | |
condition: Condition, | |
} | |
/** | |
* Ensures all child conditions must be true before this condition is true. | |
*/ | |
type AndCondition = { | |
type: 'AND', | |
conditions: Condition[], | |
} | |
/** | |
* If even one child condition is true, than this condition will be true. | |
*/ | |
type OrCondition = { | |
type: 'OR', | |
conditions: Condition[], | |
} | |
/** | |
* Checks that a named field of an object value passes a given condition. | |
*/ | |
type FieldCondition = { | |
type: 'FIELD', | |
name: string, | |
condition: Condition, | |
} | |
/** | |
* Does an equality test. If the value we are comparing against is equal to the | |
* provided value, this condition will be true. | |
* | |
* In order to use a an “in” or “one of” condition, use an `OrCondition` | |
* with multiple `EqualCondition`s. | |
* | |
* To use an “is null” operator, set `value` to `null`, for “is not null” use a | |
* `NotCondition` as well. | |
*/ | |
type EqualCondition = { | |
type: 'EQUAL', | |
value: any, | |
} | |
/** | |
* Asserts that a value is less than the provided value. The logic for | |
* comparing the values is implementation specific. | |
* | |
* For a less than or equal to condition, use the `OrCondition` with an | |
* `EqualCondition`. | |
*/ | |
type LessThanCondition = { | |
type: 'LESS_THAN', | |
value: any, | |
} | |
/** | |
* Similar to the less than condition except this condition asserts that the | |
* actual value is greater than our provided value. | |
* | |
* Use an `OrCondition` with an `EqualCondition` to get a condition for | |
* greater than or equal to. | |
*/ | |
type GreaterThanCondition = { | |
type: 'GREATER_THAN', | |
value: any, | |
} | |
/** | |
* Asserts that the actual value matches our provided regular expression. | |
*/ | |
type RegexpCondition = { | |
type: 'REGEXP', | |
regexp: RegExp, | |
} | |
/** | |
* A collection key will uniquely identify any value in a collection. They key | |
* may then be used to reliably reselect the value. A collection may have many | |
* keys, but only one primary key. | |
*/ | |
interface CollectionKey<TValue, TKey> { | |
/** | |
* Gets the collection for which this key applies. | |
*/ | |
getCollection (): Collection<TValue> | |
/** | |
* Gets the name of our collection key. | |
*/ | |
getName (): string | |
/** | |
* Gets an optional description for our collection key. | |
*/ | |
getDescription (): string | undefined | |
/** | |
* Gets the key directly from a value. Using this method we are able to get a | |
* unique key identifier for our value. A key that we can use again to | |
* operate on the value. | |
* | |
* However, not all values must have a key therefore the return type is | |
* nullable. | |
*/ | |
getKeyForValue (value: TValue): TKey | undefined | |
/** | |
* Serializes the internal representation of a key into a string. | |
*/ | |
serializeKey (key: TKey): string | |
/** | |
* Deserializes the external string into the internal representation of a | |
* key. Errors may be thrown if this is not possible. | |
*/ | |
deserializeKey (string: string): TKey | |
/** | |
* Reads a single value from the collection using that value’s key. | |
*/ | |
read (key: TKey): Promise<TValue | undefined> | |
/** | |
* Updates a value in the collection by using that value’s key. Returned is | |
* value after the updates have been applied. | |
*/ | |
// TODO: Patch object. | |
update (key: TKey): Promise<TValue> | |
/** | |
* Delete a value from the collection by using the value’s key. Returned is | |
* the value before it was deleted. | |
*/ | |
delete (key: TKey): Promise<TValue> | |
} | |
/** | |
* A relation represents a directed edge between the keys of values in two | |
* different collections. | |
* | |
* The values in the “tail” collection will be able to provide a key for a | |
* value in the “head” collection. | |
* | |
* So for example, say we had a `post` collection and a `person` collection. | |
* Let’s also say that the `post` collection has an `author` field which is a | |
* collection key for the `person` collection. In this case where `post` is | |
* related to `person,` `post` is the tail collection and `person` is the head | |
* collection. | |
* | |
* Or in relational database terms: a `post` table with an `author_id` column | |
* and a `person` table with an `id` column. The `post` table is the tail and | |
* the `person` table is the head. | |
* | |
* @see https://en.wikipedia.org/wiki/Directed_graph#Basic_terminology | |
*/ | |
export interface CollectionRelation<TTailValue, THeadValue, TKey> { | |
/** | |
* Gets the name of the relationship. | |
*/ | |
getName (): string | |
/** | |
* Gets an optional description of the relationship. | |
*/ | |
getDescription (): string | undefined | |
/** | |
* Gets the tail collection in this relationship. | |
*/ | |
getTailCollection (): Collection<TTailValue> | |
/** | |
* Gets the head collection in this relationship. | |
*/ | |
getHeadCollectionKey (): CollectionKey<THeadValue, TKey> | |
/** | |
* Gets the key for a value in the head collection from the tail collection | |
* value. | |
*/ | |
getHeadKeyFromTailValue (value: TTailValue): TKey | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment