Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
99 changes: 76 additions & 23 deletions src/CollectionStorage.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import { GraphQLNamedType, GraphQLSchema, isObjectType } from 'graphql'
import {
getNamedType,
GraphQLField,
GraphQLNamedType,
GraphQLObjectType,
GraphQLSchema,
isObjectType,
} from 'graphql'
import { DocumentCollection } from './DocumentCollection'

export type Relation = { field: string; type: string }

export class CollectionStorage<TypesMap extends Record<string, any>> {
private schema
private collections
private readonly collections
private readonly _relations

constructor(schema: GraphQLSchema) {
this.schema = schema
this.collections = initializeCollections<TypesMap>(schema)
const schemaObjectTypes = resolveSchemaObjectTypes(schema)

this.collections = initializeCollections<TypesMap>(schemaObjectTypes)
this._relations = resolveSchemaRelations<TypesMap>(schemaObjectTypes)
}

get<Type extends keyof TypesMap>(
collection<Type extends keyof TypesMap>(
type: Type,
): DocumentCollection<TypesMap[Type]> {
const collection = this.collections.get(type)
Expand All @@ -21,27 +32,25 @@ export class CollectionStorage<TypesMap extends Record<string, any>> {

return collection as DocumentCollection<TypesMap[Type]>
}
}

function initializeCollections<
TypesMap extends Record<string, any>,
TypeName extends keyof TypesMap = keyof TypesMap,
>(
schema: GraphQLSchema,
): Map<TypeName, DocumentCollection<TypesMap[TypeName]>> {
const nonRootObjectTypeNames = Object.values(schema.getTypeMap()).filter(
isNonRootObjectType,
)
relations<Type extends keyof TypesMap>(type: Type): Array<Relation> {
const relations = this._relations.get(type)

return new Map(
nonRootObjectTypeNames.map((type) => [
type.name as TypeName,
new DocumentCollection<TypesMap[TypeName]>(),
]),
)
if (!relations) {
throw new Error('Integrity Failed.')
}

return relations
}
}

function isNonRootObjectType(type: GraphQLNamedType) {
function resolveSchemaObjectTypes(schema: GraphQLSchema): GraphQLObjectType[] {
return Object.values(schema.getTypeMap()).filter(isNonRootObjectType)
}

function isNonRootObjectType(
type: GraphQLNamedType,
): type is GraphQLObjectType {
const rootTypeNames = ['Query', 'Mutation', 'Subscription']

return (
Expand All @@ -54,3 +63,47 @@ function isNonRootObjectType(type: GraphQLNamedType) {
function isInternalTypeName(typeName: string) {
return typeName.startsWith('__')
}

function resolveSchemaRelations<
TypesMap extends Record<string, any>,
TypeName extends keyof TypesMap = keyof TypesMap,
>(schemaTypes: Array<GraphQLObjectType>): Map<TypeName, Array<Relation>> {
return new Map(
schemaTypes.map((type) => {
return [type.name as TypeName, resolveTypeRelations(type)]
}),
)
}

function resolveTypeRelations(type: GraphQLObjectType): Array<Relation> {
const typeFields = type.getFields()

return Object.values(typeFields).reduce<Array<Relation>>((acc, field) => {
if (isRelationField(field)) {
return [
...acc,
{ field: field.name, type: getNamedType(field.type).name },
]
}

return acc
}, [])
}

function isRelationField(field: GraphQLField<unknown, unknown>) {
return getNamedType(field.type) instanceof GraphQLObjectType
}

function initializeCollections<
TypesMap extends Record<string, any>,
TypeName extends keyof TypesMap = keyof TypesMap,
>(
schemaTypes: Array<GraphQLObjectType>,
): Map<TypeName, DocumentCollection<TypesMap[TypeName]>> {
return new Map(
schemaTypes.map((type) => [
type.name as TypeName,
new DocumentCollection<TypesMap[TypeName]>(),
]),
)
}
4 changes: 4 additions & 0 deletions src/DocumentCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,8 @@ export class DocumentCollection<DocumentType> {
count(): number {
return this.documents.size
}

getByKey(documentKey: string) {
return this.documents.get(documentKey)
}
}
91 changes: 77 additions & 14 deletions src/Store.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,37 @@
import { GraphQLSchema } from 'graphql'
import { PredicateFunction, Schema } from './types'
import { Document, PredicateFunction, Schema } from './types'
import { CollectionStorage } from './CollectionStorage'
import { resolveGraphQLSchema } from './resolveGraphQLSchema'
import { createDocument } from './createDocument'
import { getDocumentKey, getDocumentType, isDocument } from './utils'
import {
createDocumentRef,
generateDocumentKey,
getDocumentKey,
getDocumentType,
isDocument,
isDocumentRef,
} from './utils'
import { DOCUMENT_KEY, DOCUMENT_TYPE } from './constants'

interface StoreConfiguration {
schema: Schema
}

export class Store<TypesMap extends Record<string, any>> {
protected collections
protected storage
protected schema: GraphQLSchema

constructor(config: StoreConfiguration) {
this.schema = resolveGraphQLSchema(config.schema)
this.collections = new CollectionStorage<TypesMap>(this.schema)
this.storage = new CollectionStorage<TypesMap>(this.schema)
}

create<Type extends keyof TypesMap>(
type: Type,
data: TypesMap[Type],
): TypesMap[Type] {
const document = createDocument(type as string, data)
return this.collections.get(type).create(document)
const document = this.createDocument(type, data)

return this.storage.collection(type).create(document)
}

update<Type extends TypesMap[keyof TypesMap]>(
Expand All @@ -37,10 +45,10 @@ export class Store<TypesMap extends Record<string, any>> {
const type = getDocumentType(document)
const key = getDocumentKey(document)

return this.collections
.get(type)
return this.storage
.collection(type)
.create(
createDocument<TypesMap[string]>(
this.createDocument(
type,
{ ...document, ...data } as TypesMap[string],
key,
Expand All @@ -52,7 +60,7 @@ export class Store<TypesMap extends Record<string, any>> {
type: Type,
predicate?: PredicateFunction<TypesMap[Type]>,
): TypesMap[Type] | undefined {
return this.collections.get(type).findFirst(predicate)
return this.storage.collection(type).findFirst(predicate)
}

findFirstOrThrow<Type extends keyof TypesMap>(
Expand All @@ -72,14 +80,69 @@ export class Store<TypesMap extends Record<string, any>> {
type: Type,
predicate?: PredicateFunction<TypesMap[Type]>,
): Array<TypesMap[Type]> {
return this.collections.get(type).find(predicate)
return this.storage.collection(type).find(predicate)
}

count<Type extends keyof TypesMap>(type: Type): number {
return this.collections.get(type).count()
return this.storage.collection(type).count()
}

reset(): void {
this.collections = new CollectionStorage<TypesMap>(this.schema)
this.storage = new CollectionStorage<TypesMap>(this.schema)
}

protected createDocument<Type extends keyof TypesMap>(
type: Type,
data: TypesMap[Type],
documentKey?: string,
): Document<TypesMap[Type]> {
const document = structuredClone(data)

Reflect.defineProperty(document, DOCUMENT_KEY, {
enumerable: false,
configurable: false,
writable: false,
value: documentKey || generateDocumentKey(),
})

Reflect.defineProperty(document, DOCUMENT_TYPE, {
enumerable: false,
configurable: false,
writable: false,
value: type,
})

this.storage.relations(type).forEach(({ field, type }) => {
if (Array.isArray(document[field])) {
// TODO Implement one-to-many relation
return
}

const targetDocument = this.create(type, document[field])
Object.defineProperty(document, field, {
value: createDocumentRef(
getDocumentKey(targetDocument),
getDocumentType(targetDocument),
),
})
})

return new Proxy(document, {
get: (target, prop) => {
const data = Reflect.get(document, prop)

if (isDocumentRef(data)) {
return this.storage.collection(data.$ref.type).getByKey(data.$ref.key)
}

if (Reflect.has(document, prop) || typeof prop !== 'string') {
return Reflect.get(document, prop)
}
},

set() {
throw new Error('Documents are immutable.')
},
})
}
}
47 changes: 0 additions & 47 deletions src/createDocument.ts

This file was deleted.

2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export type Document<DocumentType> = DocumentType & {
}

export type PredicateFunction<T> = (data: T) => boolean

export type DocumentRef = { $ref: { key: string; type: string } }
17 changes: 16 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Document } from './types'
import { nanoid } from 'nanoid'
import { Document, DocumentRef } from './types'
import { DOCUMENT_KEY, DOCUMENT_TYPE } from './constants'

export function getDocumentKey<T>(document: Document<T>) {
Expand All @@ -17,3 +18,17 @@ export function isDocument(document: unknown): document is Document<unknown> {
Reflect.has(document, DOCUMENT_TYPE)
)
}

export function generateDocumentKey() {
return nanoid(16)
}

export function createDocumentRef(key: string, type: string): DocumentRef {
return { $ref: { key, type } }
}

export function isDocumentRef(input: unknown): input is DocumentRef {
return (
input !== null && typeof input === 'object' && Reflect.has(input, '$ref')
)
}
Loading