Skip to content

Instantly share code, notes, and snippets.

@calebmer
Created July 17, 2016 14:39
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save calebmer/c26444edd35e29ff3b612966c3f982f3 to your computer and use it in GitHub Desktop.
Save calebmer/c26444edd35e29ff3b612966c3f982f3 to your computer and use it in GitHub Desktop.
// 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