Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support nested objects and arrays in the model definition #113

Merged
merged 7 commits into from
Oct 11, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@
"typescript": "4.3.5"
},
"dependencies": {
"@types/lodash.get": "^4.4.6",
"@types/lodash": "^4.14.172",
"@types/md5": "^2.3.0",
"@types/pluralize": "^0.0.29",
"@types/uuid": "^8.3.0",
"date-fns": "^2.21.1",
"graphql": "^15.5.0",
"lodash.get": "^4.4.2",
"lodash": "^4.17.21",
"md5": "^2.3.0",
"pluralize": "^8.0.0",
"strict-event-emitter": "^0.2.0",
Expand Down
44 changes: 21 additions & 23 deletions src/glossary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from './query/queryTypes'

export type PrimaryKeyType = string | number
export type BaseTypes = string | number | boolean | Date
export type PrimitiveValueType = string | number | boolean | Date
export type KeyType = string | number | symbol

export enum InternalEntityProperty {
Expand All @@ -17,7 +17,7 @@ export enum InternalEntityProperty {
}

export interface PrimaryKeyDeclaration<
ValueType extends PrimaryKeyType = string
ValueType extends PrimaryKeyType = string,
> {
isPrimaryKey: boolean
getValue(): ValueType
Expand All @@ -34,7 +34,7 @@ export enum RelationKind {
*/
export interface RelationDefinition<
Kind extends RelationKind,
ModelName extends KeyType
ModelName extends KeyType,
> {
kind: Kind
unique: boolean
Expand All @@ -52,11 +52,10 @@ export interface Relation {
* Minimal representation of an entity to look it up
* in the database and resolve upon reference.
*/
export type RelationRef<
ModelName extends string
> = InternalEntityProperties<ModelName> & {
[InternalEntityProperty.nodeId]: PrimaryKeyType
}
export type RelationRef<ModelName extends string> =
InternalEntityProperties<ModelName> & {
[InternalEntityProperty.nodeId]: PrimaryKeyType
}

export interface RelationOptions {
unique: boolean
Expand All @@ -74,7 +73,8 @@ export type ManyOf<ModelName extends KeyType> = RelationDefinition<

export type ModelDefinition = Record<
string,
(() => BaseTypes) | OneOf<any> | ManyOf<any> | PrimaryKeyDeclaration
PrimaryKeyDeclaration | OneOf<any> | ManyOf<any> | (() => PrimitiveValueType)
// | Record<string, unknown>
>

export type FactoryAPI<Dictionary extends Record<string, any>> = {
Expand All @@ -88,20 +88,20 @@ export interface InternalEntityProperties<ModelName extends KeyType> {

export type Entity<
Dictionary extends ModelDictionary,
ModelName extends keyof Dictionary
ModelName extends keyof Dictionary,
> = Value<Dictionary[ModelName], Dictionary>

export type InternalEntity<
Dictionary extends ModelDictionary,
ModelName extends keyof Dictionary
ModelName extends keyof Dictionary,
> = InternalEntityProperties<ModelName> & Entity<Dictionary, ModelName>

export type ModelDictionary = Limit<Record<string, Record<string, any>>>

export type Limit<T extends Record<string, any>> = {
[RK in keyof T]: {
[SK in keyof T[RK]]: T[RK][SK] extends
| (() => BaseTypes)
| (() => PrimitiveValueType)
| PrimaryKeyDeclaration
| OneOf<keyof T>
| ManyOf<keyof T>
Expand All @@ -115,24 +115,22 @@ export type Limit<T extends Record<string, any>> = {

export type RequireExactlyOne<
ObjectType,
KeysType extends keyof ObjectType = keyof ObjectType
KeysType extends keyof ObjectType = keyof ObjectType,
> = {
[Key in KeysType]: Required<Pick<ObjectType, Key>> &
Partial<Record<Exclude<KeysType, Key>, never>>
}[KeysType] &
Pick<ObjectType, Exclude<keyof ObjectType, KeysType>>

export type DeepRequireExactlyOne<ObjectType> = RequireExactlyOne<
{
[K in keyof ObjectType]: ObjectType[K] extends Record<any, any>
? RequireExactlyOne<ObjectType[K]>
: ObjectType[K]
}
>
export type DeepRequireExactlyOne<ObjectType> = RequireExactlyOne<{
[K in keyof ObjectType]: ObjectType[K] extends Record<any, any>
? RequireExactlyOne<ObjectType[K]>
: ObjectType[K]
}>

export interface ModelAPI<
Dictionary extends ModelDictionary,
ModelName extends keyof Dictionary
ModelName extends keyof Dictionary,
> {
/**
* Create a single entity for the model.
Expand Down Expand Up @@ -206,7 +204,7 @@ export interface ModelAPI<

export type UpdateManyValue<
T extends Record<string, any>,
Parent extends Record<string, any>
Parent extends Record<string, any>,
> =
| Value<T, Parent>
| {
Expand All @@ -223,7 +221,7 @@ export type UpdateManyValue<

export type Value<
T extends Record<string, any>,
Parent extends Record<string, any>
Parent extends Record<string, any>,
> = {
[K in keyof T]: T[K] extends OneOf<any>
? Entity<Parent, T[K]['modelName']>
Expand Down
57 changes: 33 additions & 24 deletions src/model/createModel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { debug } from 'debug'
import get from 'lodash/get'
import set from 'lodash/set'
import isFunction from 'lodash/isFunction'
import { Database } from '../db/Database'
import {
InternalEntity,
Expand All @@ -15,7 +18,7 @@ const log = debug('createModel')

export function createModel<
Dictionary extends ModelDictionary,
ModelName extends string
ModelName extends string,
>(
modelName: ModelName,
definition: ModelDefinition,
Expand All @@ -33,52 +36,58 @@ export function createModel<
initialValues,
)

// Internal properties that allow identifying this model
// when referenced in other models (i.e. via relatioships).
const internalProperties: InternalEntityProperties<ModelName> = {
[InternalEntityProperty.type]: modelName,
[InternalEntityProperty.primaryKey]: primaryKey,
}

const resolvedProperties = properties.reduce<Record<string, any>>(
(entity, property) => {
const exactValue = initialValues[property]
const propertyDefinition = definition[property]

log(
`property definition for "${modelName}.${property}"`,
propertyDefinition,
)
const publicProperties = properties.reduce<Record<string, unknown>>(
(properties, propertyName) => {
const value = get(initialValues, propertyName)
const propertyDefinition = get(definition, propertyName)

// Ignore relational properties at this stage.
if ('kind' in propertyDefinition) {
return entity
return properties
}

if ('isPrimaryKey' in propertyDefinition) {
entity[property] = exactValue || propertyDefinition.getValue()
return entity
set(properties, propertyName, value || propertyDefinition.getValue())
return properties
}

if (
typeof exactValue === 'string' ||
typeof exactValue === 'number' ||
typeof exactValue === 'boolean' ||
exactValue?.constructor.name === 'Date'
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean' ||
value?.constructor.name === 'Date' ||
Array.isArray(value)
) {
log(`"${modelName}.${property}" has a plain initial value:`, exactValue)
entity[property] = exactValue
return entity
log(
'"%s" has a plain initial value:',
`${modelName}.${propertyName}`,
value,
)
set(properties, propertyName, value)
return properties
}

if (isFunction(propertyDefinition)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was your intention behind adding this explicit isFunction check?

Type-wise, at this point of processing propertyDefinition is a base type getter (() => BaseType), in other words: always a function. Perhaps you wanted to guard this reduce against malformed user input?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After 2 months I don't exactly remember tbh. Probably what you guessed, otherwise something related to the very first implementation of the feature. It's probably redundant at this point.

Btw was just looking at line 66, where I wonder if logging "has a plain initial value" is still true for arrays (which now are an option)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries, I'm remembering everything around this myself.

looking at line 66, where I wonder if logging "has a plain initial value" is still true for arrays

Is it the wording that confuses you? I'd say it matters little as it's internal logging.

set(properties, propertyName, propertyDefinition())
return properties
}

entity[property] = propertyDefinition()
return entity
return properties
},
{},
)

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

log(`created "${modelName}" entity`, entity)
log('created "%s" entity', modelName, entity)

return entity
}
44 changes: 38 additions & 6 deletions src/model/defineRelationalProperties.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { debug } from 'debug'
import get from 'lodash/get'
import set from 'lodash/set'
import { isObject } from '../utils/isObject'
import { Database } from '../db/Database'
import {
Entity,
Expand Down Expand Up @@ -37,10 +40,12 @@ export function defineRelationalProperties(
relation,
)

if (!(property in initialValues)) return properties
if (!get(initialValues, property)) return properties

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

if (relation.unique) {
log(`verifying that the "${property}" relation is unique...`)
Expand Down Expand Up @@ -83,7 +88,7 @@ export function defineRelationalProperties(
}
}

properties[property] = {
set(properties, property, {
enumerable: true,
get() {
log(`get "${property}"`, relation)
Expand All @@ -107,17 +112,44 @@ export function defineRelationalProperties(
},
[],
)

log(`resolved "${relation.kind}" "${property}" to`, refValue)

return relation.kind === RelationKind.OneOf
? first(refValue)!
: refValue
},
}

})
return properties
},
{},
)
Object.defineProperties(entity, properties)
defineNestedProperties(entity, properties, '')
}

function defineNestedProperties(
entity: InternalEntity<any, any>,
properties: any,
path: string,
) {
for (let key in properties) {
const value = properties[key]
const nestedPath = path ? `${path}.${key}` : key

if (isRelationalProperty(value)) {
const nestedPathArray = nestedPath.split('.')
nestedPathArray.reduce((acc, curr, i) => {
if (i === nestedPathArray.length - 1) {
Object.defineProperty(acc, curr, value)
kettanaito marked this conversation as resolved.
Show resolved Hide resolved
}
return acc[curr]
}, entity)
} else if (isObject(value)) {
defineNestedProperties(entity, value, nestedPath)
}
}
}

function isRelationalProperty(val: any) {
kettanaito marked this conversation as resolved.
Show resolved Hide resolved
return val?.enumerable && typeof val?.get === 'function'
}
Loading