Skip to content

Commit

Permalink
feat: re-design the "Relation" class
Browse files Browse the repository at this point in the history
  • Loading branch information
kettanaito committed Oct 13, 2021
1 parent 9f88f0d commit 208ddb2
Show file tree
Hide file tree
Showing 17 changed files with 664 additions and 404 deletions.
5 changes: 3 additions & 2 deletions src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ function createModelApi<
const entity = createModel<Dictionary, ModelName>(
modelName,
definition,
dictionary,
parsedModel,
initialValues,
db,
Expand Down Expand Up @@ -163,7 +164,7 @@ function createModelApi<
return null
}

const nextRecord = updateEntity(prevRecord, query.data, definition, db)
const nextRecord = updateEntity(prevRecord, query.data, definition)

if (
nextRecord[prevRecord[InternalEntityProperty.primaryKey]] !==
Expand Down Expand Up @@ -211,7 +212,7 @@ function createModelApi<
}

records.forEach((prevRecord) => {
const nextRecord = updateEntity(prevRecord, query.data, definition, db)
const nextRecord = updateEntity(prevRecord, query.data, definition)

if (
nextRecord[prevRecord[InternalEntityProperty.primaryKey]] !==
Expand Down
23 changes: 5 additions & 18 deletions src/glossary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
QuerySelector,
WeakQuerySelector,
} from './query/queryTypes'
import { RelationKind, ManyOf, OneOf } from './relations/Relation'
import { OneOf, ManyOf } from './relations/Relation'

export type KeyType = string | number | symbol
export type AnyObject = Record<KeyType, any>
Expand All @@ -15,19 +15,6 @@ export type PrimitiveValueType = string | number | boolean | Date
export type ModelValueType = PrimitiveValueType | PrimitiveValueType[]
export type ModelValueTypeGetter = () => ModelValueType

/**
* Definition of the relation.
* @example factory({ user: { post: oneOf('post') } })
*/
export interface RelationDefinition<
Kind extends RelationKind,
ModelName extends KeyType,
> {
kind: Kind
unique: boolean
modelName: ModelName
}

/**
* Minimal representation of an entity to look it up
* in the database and resolve upon reference.
Expand Down Expand Up @@ -216,10 +203,10 @@ export type Value<
[Key in keyof Target]: Target[Key] extends PrimaryKey<any>
? ReturnType<Target[Key]['getValue']>
: // Extract value type from relations.
Target[Key] extends OneOf<any>
? Entity<Dictionary, Target[Key]['modelName']>
: Target[Key] extends ManyOf<any>
? Entity<Dictionary, Target[Key]['modelName']>[]
Target[Key] extends OneOf<infer ModelName>
? Entity<Dictionary, ModelName>
: Target[Key] extends ManyOf<infer ModelName>
? Entity<Dictionary, ModelName>[]
: // Account for primitive value getters because
// native constructors (i.e. StringConstructor) satisfy
// the "AnyObject" predicate below.
Expand Down
5 changes: 3 additions & 2 deletions src/model/createModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export function createModel<
>(
modelName: ModelName,
definition: ModelDefinition,
dictionary: Dictionary,
parsedModel: ParsedModelDefinition,
initialValues: Partial<Value<Dictionary[ModelName], Dictionary>>,
db: Database<Dictionary>,
Expand Down Expand Up @@ -92,9 +93,9 @@ export function createModel<
)

const entity = Object.assign({}, publicProperties, internalProperties)
defineRelationalProperties(entity, initialValues, relations, db)
defineRelationalProperties(entity, initialValues, relations, dictionary, db)

log('created "%s" entity', modelName, entity)
log('created "%s" entity:', modelName, entity)

return entity
}
146 changes: 8 additions & 138 deletions src/model/defineRelationalProperties.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,16 @@
import { debug } from 'debug'
import get from 'lodash/get'
import set from 'lodash/set'
import { invariant } from 'outvariant'
import { Database } from '../db/Database'
import {
Entity,
InternalEntity,
InternalEntityProperty,
ModelDictionary,
Value,
} from '../glossary'
import { executeQuery } from '../query/executeQuery'
import { first } from '../utils/first'
import { definePropertyAtPath } from '../utils/definePropertyAtPath'
import {
ProducedRelationsMap,
ProducedRelation,
RelationKind,
} from '../relations/Relation'
import { QuerySelectorWhere } from '../query/queryTypes'
import { InternalEntity, ModelDictionary, Value } from '../glossary'
import { RelationsMap } from '../relations/Relation'

const log = debug('defineRelationalProperties')

export function defineRelationalProperties(
entity: InternalEntity<any, any>,
initialValues: Partial<Value<any, ModelDictionary>>,
relations: ProducedRelationsMap,
relations: RelationsMap,
dictionary: ModelDictionary,
db: Database<any>,
): void {
log('defining relational properties...', { entity, initialValues, relations })
Expand All @@ -41,125 +26,10 @@ export function defineRelationalProperties(
continue
}

// Take the relational entity reference from the initial values.
const entityRefs: Entity<any, any>[] = [].concat(
get(initialValues, propertyPath),
)

log('entity references:', entityRefs)

if (relation.unique) {
log('"%s" is a unique relation, verifying..."', propertyPath)

// Trying to look up an entity of the same type
// that references the same relational entity.
const existingEntities = executeQuery(
entity[InternalEntityProperty.type],
entity[InternalEntityProperty.primaryKey],
{
where: set<QuerySelectorWhere<any>>({}, propertyPath, {
[relation.primaryKey]: {
in: entityRefs.map((entityRef) => {
return entityRef[relation.primaryKey]
}),
},
}),
},
db,
)

log(
`existing entities that reference the same "${propertyPath}"`,
existingEntities,
)

invariant(
existingEntities.length === 0,
'Failed to create a unique "%s" relation for "%s" (%s): the provided entity is already used.',
relation.modelName,
`${entity.__type}.${propertyPath}`,
entity[entity[InternalEntityProperty.primaryKey]],
)
}

addRelation(entity, propertyPath, relation, entityRefs, db)

log('relation "%s" successfuly set!', propertyPath)
}
}

export function addRelation(
entity: Entity<any, any>,
propertyPath: string,
relation: ProducedRelation,
references: Value<any, any> | Value<any, any>[],
db: Database<any>,
): void {
const entityType = entity[InternalEntityProperty.type]
const referencesList = ([] as Value<any, any>[]).concat(references)
const referencedModels = db.getModel(relation.modelName)

log(
'adding a "%s" relational property "%s" on "%s" (%j)',
relation.kind,
propertyPath,
entityType,
references,
)
log(
'database records for the referenced "%s" model:',
relation.modelName,
referencedModels,
)

// All referenced entities must exist.
// This also guards against providing compatible plain objects as next values,
// because they won't have the corresponding records in the database.
referencesList.forEach((reference) => {
const referenceId = reference[relation.primaryKey]
invariant(
referencedModels.has(referenceId),
'Failed to add relational property "%s" on "%s": referenced entity with the id "%s" does not exist.',
const references: Value<any, ModelDictionary> = get(
initialValues,
propertyPath,
entityType,
referenceId,
)
})

definePropertyAtPath(entity, propertyPath, {
enumerable: true,
// Mark the property as configurable so that it can be re-defined.
// Relational properties may be re-defined when updated during the
// entity update ("update"/"updateMany").
configurable: true,
get() {
log(`get "${propertyPath}"`, relation)

const queryResult = referencesList.reduce<InternalEntity<any, any>[]>(
(result, entityRef) => {
return result.concat(
executeQuery(
relation.modelName,
relation.primaryKey,
{
where: {
[relation.primaryKey]: {
equals: entityRef[relation.primaryKey],
},
},
},
db,
),
)
},
[],
)

log(`resolved "${relation.kind}" "${propertyPath}" to`, queryResult)

return relation.kind === RelationKind.OneOf
? first(queryResult)
: queryResult
},
})
relation.apply(entity, propertyPath, references, dictionary, db)
}
}
6 changes: 3 additions & 3 deletions src/model/parseModelDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { invariant } from 'outvariant'
import { ModelDefinition, PrimaryKeyType, ModelDictionary } from '../glossary'
import { PrimaryKey } from '../primaryKey'
import { isObject } from '../utils/isObject'
import { Relation, ProducedRelationsMap } from '../relations/Relation'
import { Relation, RelationsMap } from '../relations/Relation'

const log = debug('parseModelDefinition')

export interface ParsedModelDefinition {
primaryKey: PrimaryKeyType
properties: string[]
relations: ProducedRelationsMap
relations: RelationsMap
}

/**
Expand Down Expand Up @@ -68,7 +68,7 @@ function deepParseModelDefinition<Dictionary extends ModelDictionary>(
if (value instanceof Relation) {
// Resolve a relation against the dictionary to collect
// the primary key names of the referenced models.
result.relations[propertyPath] = value.produce(dictionary)
result.relations[propertyPath] = value
continue
}

Expand Down
Loading

0 comments on commit 208ddb2

Please sign in to comment.